-
Notifications
You must be signed in to change notification settings - Fork 251
feat(scheduler,admission): implement in-place pod resize accounting and queue admission #2021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
1defb16
docs: propose in-place pod resize accounting and queue admission
gshaibi 2730de2
feat(scheduler,admission): implement in-place pod resize accounting a…
gshaibi d0b11ff
chore: add changelog fragment for in-place pod resize feature
gshaibi 9472372
fix: remove unused subResourceList from effective_requests
gshaibi b0e2133
fix(scheduler,admission): address code review findings
gshaibi b758106
feat(admission): add validateQuota and blockUpsizeOnBoundedQueues to …
gshaibi 04bfdd7
chore: fix formatting, generated CRD/RBAC, and trailing newline for m…
gshaibi 6cadbf6
fix(admission): fix podResizeDelta for sidecars, non-infeasible basel…
gshaibi 268b3ee
Merge remote-tracking branch 'origin/main' into claude/pr-1997-implem…
gshaibi ee61814
style: gofmt pod_transform.go after merge resolution
gshaibi 68ed5e4
prototype: delegate effective-request accounting to k8s.io/component-…
gshaibi c08f731
prototype: delegate admission delta to upstream aggregates
gshaibi aacb74d
test: pin upstream IsPodResizeInfeasible semantics as a characterizat…
gshaibi 7ddb95c
Merge pull request #2036 from kai-scheduler/claude/upstream-resource-…
gshaibi 4dab44e
Merge remote-tracking branch 'origin/main' into claude/pr-1997-implem…
gshaibi 2c7c113
test(scheduler): rename effective_requests_test.go to pod_resize_acco…
gshaibi c8086d9
docs(scheduler): explain why UseStatusResources is not gated on InPla…
gshaibi dbc7e4f
refactor(scheduler,admission): rename compactConditions, document fla…
gshaibi d59c0a0
fix(pod-group-controller): charge effective requests in podgroup allo…
gshaibi 2bf817f
refactor(admission,pod-group-controller): share preemptibility resolu…
gshaibi 7a01279
test(e2e),docs: add in-place pod resize e2e suite and user documentation
gshaibi 8303746
docs: cross-link queue limits and in-place resize docs
gshaibi dd6984e
fix(e2e): create resize suite TestContext inside each It
gshaibi 374982d
refactor(admission): convert pod resize validator to typed Validator
gshaibi 3cf19b6
chore: changelog fragment for queue allocated accounting change
gshaibi e667832
docs: state resize admission is best-effort in the intro, not a stric…
gshaibi bb12595
docs: resize admission also races the scheduler, not only concurrent …
gshaibi bfabb50
style: trim non-essential code comments
gshaibi 9a849f7
.
gshaibi ab63e5c
docs: resize during a pending downsize is checked against the transie…
gshaibi 1c04a72
style: strip comments from pod resize validator
gshaibi c397fd9
style: trim remaining resize accounting comments
gshaibi b940444
docs(design): align Infeasible semantics with implementation, pin Def…
gshaibi 59b5246
docs(admission): clarify resize endpoint separation and best-effort s…
gshaibi b44d055
docs(design): mark in-place resize design as implemented
gshaibi c7d864a
docs(design): drop Volcano reference
gshaibi c5991d1
docs: drop in-place resize bullet from main README
gshaibi 19cda79
test(e2e): restructure resize suite to house pattern, pin kai config
gshaibi c4a03fa
test(operator): spell out failurePolicy semantics in webhook assertio…
gshaibi 9ae0efc
CR fixes
gshaibi 022046f
PR fix
gshaibi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| kind: Added | ||
| body: |- | ||
| In-place pod resize accounting (KEP-1287) and queue admission webhook |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| kind: Changed | ||
| body: |- | ||
| Queue allocated status now includes init-container peak and sidecar requests | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| <!-- | ||
| Copyright 2026 NVIDIA CORPORATION | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| --> | ||
|
|
||
| # In-Place Pod Resize Accounting and Queue Admission | ||
|
|
||
| *Status: Implemented* | ||
|
|
||
| Related issues: [#1906](https://github.com/kai-scheduler/KAI-Scheduler/issues/1906), | ||
| [#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872) (deferred resize eviction — separate track) | ||
|
|
||
| ## Motivation | ||
|
|
||
| In-place resize (`pods/resize`) lets a running Pod change CPU/memory without | ||
| rescheduling. KAI has three gaps: | ||
|
|
||
| 1. **Accounting.** KAI charges Pod resources from the spec. While a resize is | ||
| pending, infeasible, or in progress, spec ≠ occupancy. That creates phantom | ||
| capacity on downsize, overcharges on infeasible upsize, and corrupts queue | ||
| allocation, fair share, reclaim, and reporting. | ||
| 2. **Queue admission.** Resize bypasses scheduling. A Pod can grow past queue | ||
| `limit`, or past deserved `quota` when non-preemptible. The validating | ||
| webhook matches `pods`, not `pods/resize`, so it never sees an old→new | ||
| delta. | ||
| 3. **Deferred resize eviction.** When the kubelet marks a resize `Deferred` | ||
| for node capacity, KAI does not preempt or reclaim on that node to make | ||
| room. Out of scope here — tracked in | ||
| [#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872). | ||
|
|
||
| This design covers (1) and (2). Upstream ResourceQuota already admits resize | ||
| via status-aware requests and a positive usage delta; KAI needs the same | ||
| effective-request model and a hierarchical check. | ||
|
|
||
| ## Design | ||
|
|
||
| ### Effective accounting | ||
|
|
||
| Use the upstream effective request as KAI's default Pod resource vector. Per | ||
| container and resource, before Pod-level aggregation: | ||
|
|
||
| ```text | ||
| normal / Deferred / in progress: | ||
| effective = max(spec request, allocatedResources, status.resources) | ||
|
|
||
| Infeasible: | ||
| effective = max(allocatedResources, status.resources) | ||
| ``` | ||
|
|
||
| Drive node fit, queue `Allocated` / `AllocatedNotPreemptible`, fair share, | ||
| ordering, reclaim, victim selection, and allocated status/metrics from this | ||
| vector. Do not plumb a separate desired-resource vector through the | ||
| scheduler; read the raw spec only for intent (proposed target, user-facing | ||
| desired fields, infeasible diagnostics). | ||
|
|
||
| **`Infeasible` detection** delegates to upstream | ||
| `resource.IsPodResizeInfeasible`, which keys off the condition reason alone. | ||
| The kubelet owns the condition lifecycle — it clears or replaces | ||
| `PodResizePending` when a new resize is submitted — so generation tracking is | ||
| not needed, and `observedGeneration` is only populated behind the non-GA | ||
| `PodObservedGenerationTracking` gate (a guard on it would disable `Infeasible` | ||
| handling on gate-off clusters). Upstream semantics are pinned by a | ||
| characterization test so a future upstream tightening surfaces as a failure. | ||
|
|
||
| The `Deferred` charge at `max(spec, actual)` is load-bearing beyond | ||
| accounting: deferred-resize eviction | ||
| ([#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872), | ||
| [#2051](https://github.com/kai-scheduler/KAI-Scheduler/pull/2051)) relies on | ||
| the deferred target already being reserved in node and queue accounting, so | ||
| capacity freed by evicting victims is not backfilled before the kubelet | ||
| enacts the resize. Charging `Deferred` at actual only would reintroduce | ||
| eviction thrash. | ||
|
|
||
| Implementation: upstream `resource.AggregateContainerRequests` (status | ||
| resources enabled), then KAI custom-resource logic. | ||
|
|
||
| ### Best-effort resize quota admission | ||
|
|
||
| Validating webhook on `UPDATE`/`pods/resize` with old and proposed Pods. For | ||
| KAI-scheduled Pods: | ||
|
|
||
| 1. Resolve PodGroup, preemptibility, leaf queue, and ancestors. | ||
| 2. Compute old and proposed effective requests (inherited `Infeasible` stale). | ||
| 3. `delta = max(proposed - old, 0)` per resource. | ||
| 4. Reject if any queue on the path would violate: | ||
|
|
||
| ```text | ||
| all workloads: | ||
| Allocated + delta > limit | ||
|
|
||
| non-preemptible: | ||
| AllocatedNotPreemptible + delta > quota (deserved) | ||
| ``` | ||
|
|
||
| Usage comes from the best-available snapshot (Queue status / live effective | ||
| requests). Downsize always admissible; capacity frees only when effective | ||
| usage falls. Limit-only changes with no request growth have zero delta. | ||
| Rejections name Pod, PodGroup, limiting queue/ancestor, resource, allocation, | ||
| delta, and boundary. | ||
|
|
||
| This is **not** atomic across concurrent resizes or against in-flight | ||
| scheduler allocations. Existing violations (race, bypass, pre-rollout) keep | ||
| full effective accounting, are reported, and block further deepenings via the | ||
| webhook and allocate-time capacity checks. No proactive "drain until under | ||
| limit" action for now. | ||
|
|
||
| ### Optional strict upsize block | ||
|
|
||
| Config knob, **disabled by default**, rejects request upsizes that could race | ||
| past a hard queue bound — even if current usage would still fit: | ||
|
|
||
| - **Any** upsize, if the leaf queue or an ancestor has a finite `limit` on that | ||
| resource. | ||
| - **Non-preemptible** upsize, if the leaf queue or an ancestor has a finite | ||
| `quota` (deserved) on that resource. | ||
|
|
||
| Escape hatch for operators who cannot tolerate best-effort races without a | ||
| ledger. Preemptible upsizes on queues that are only bounded by deserved (no | ||
| finite limit) remain subject to the best-effort check only — they may go over | ||
| deserved by design. | ||
|
|
||
| ```yaml | ||
| spec: | ||
| admission: | ||
| inPlacePodResize: | ||
| validateQuota: true # best-effort hierarchical checks | ||
| blockUpsizeOnBoundedQueues: false # conservative; default off | ||
| ``` | ||
|
|
||
| Exact CR field names TBD at implementation. | ||
|
|
||
| ## Decisions | ||
|
|
||
| | Topic | Decision | | ||
| | --- | --- | | ||
| | Accounting model | Upstream effective request; upstream reason-only `Infeasible` semantics (kubelet owns the condition lifecycle) | | ||
| | Resize admit path | Keep native `pods/resize`; validate in webhook (do **not** convert to a KAI-owned scheduling API — breaks VPA / API contract) | | ||
| | Non-preemptible growth past quota | Reject at webhook; keep reject at allocate | | ||
| | Concurrent-resize / scheduler races | Best effort; optional `blockUpsizeOnBoundedQueues`; reservation ledger only if a concrete issue appears | | ||
| | Drain-until-under-limit action | Rejected — poor UX; reclaim stays demand-driven | | ||
| | 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 | | ||
| | 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)) | | ||
|
|
||
| ## Known gaps | ||
|
|
||
| Best-effort admission can race: concurrent upsizes, or a resize vs scheduler | ||
| allocate, may double-spend the same remaining headroom. | ||
| `blockUpsizeOnBoundedQueues` and allocate/webhook deepenings mitigate today. | ||
| Prefer upstream resize gates | ||
| ([kubernetes#131835](https://github.com/kubernetes/kubernetes/issues/131835)) | ||
| as the long-term out-of-tree quota hook; add a reservation ledger only if | ||
| gates do not land and races hurt in production. | ||
|
|
||
| ## References | ||
|
|
||
| - [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) | ||
| - [Kubernetes 1.35 in-place container resize](https://v1-35.docs.kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) | ||
| - [Upstream effective Pod request helper](https://github.com/kubernetes/kubernetes/blob/v1.35.4/staging/src/k8s.io/component-helpers/resource/helpers.go) | ||
| - [Upstream Pod quota evaluator](https://github.com/kubernetes/kubernetes/blob/v1.35.4/pkg/quota/v1/evaluator/core/pods.go) | ||
| - [KEP-5836: Scheduler Preemption for In-Place Pod Resize](https://www.kubernetes.dev/resources/keps/5836) | ||
| - [kubernetes#131835: out-of-tree quota vs in-place resize](https://github.com/kubernetes/kubernetes/issues/131835) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # In-Place Pod Resize | ||
|
|
||
| KAI Scheduler supports Kubernetes [in-place pod resize](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) (KEP-1287): changing a running pod's CPU and memory requests without restarting it. KAI keeps queue accounting consistent throughout a resize, and applies a best-effort admission check of resize requests against queue limits and quota — a guardrail rather than a strict guarantee (see [Best-effort semantics](#best-effort-semantics)). | ||
|
|
||
| Requires Kubernetes 1.33 or newer (the `InPlacePodVerticalScaling` feature gate is enabled by default since 1.33 and GA since 1.35). | ||
|
|
||
| ## Resizing a pod | ||
|
|
||
| Resizes go through the `pods/resize` subresource: | ||
|
|
||
| ```bash | ||
| kubectl patch pod my-pod --subresource resize \ | ||
| -p '{"spec":{"containers":[{"name":"main","resources":{"requests":{"cpu":"2"}}}]}}' | ||
| ``` | ||
|
|
||
| Only CPU and memory can be resized; GPU resources cannot be changed in place. | ||
|
|
||
| ## Accounting: what a resizing pod is charged | ||
|
|
||
| During a resize, a pod's desired requests (spec) and its actual running size can differ. KAI charges the **effective request** per container and resource: | ||
|
|
||
| | Resize state | Charged amount | | ||
| |---|---| | ||
| | No resize in progress | spec | | ||
| | Resize pending or in progress | max(spec, actual) | | ||
| | Resize marked **Infeasible** by the kubelet | actual only — the unreachable spec is not charged | | ||
|
|
||
| "Actual" is the value the kubelet reports in the container status. This model matches the upstream Kubernetes scheduler and kubelet, and applies consistently across: | ||
|
|
||
| - scheduler decisions (fair share, node fit, preemption and reclaim), | ||
| - queue status (`Queue.status.allocated`, `Queue.status.allocatedNonPreemptible`), | ||
| - resize admission (below). | ||
|
|
||
| The practical consequences: | ||
|
|
||
| - A pod mid-downsize keeps its current (larger) charge until the kubelet completes the downsize — the freed capacity is never promised before it exists. | ||
| - A pod whose upsize the kubelet rejected as Infeasible does not hold queue capacity for a size it will never reach. | ||
|
|
||
| ## Resize admission | ||
|
|
||
| A validating webhook on `pods/resize` checks every upsize of a KAI-scheduled pod against the pod's queue hierarchy, before the resize is persisted: | ||
|
|
||
| - **Limit** (all workloads): the increase must not push any queue's `allocated` over its CPU or memory `limit`. | ||
| - **Quota** (non-preemptible workloads only): the increase must also fit within `quota`, checked against `allocatedNonPreemptible`. | ||
|
|
||
| The increase is computed at the pod level, so moving CPU or memory between containers of the same pod is not counted as growth, and downsizes are always admitted. | ||
|
|
||
| A rejected resize returns an error such as: | ||
|
|
||
| ``` | ||
| resize rejected: pod team-a/trainer (PodGroup pg-trainer) CPU upsize would push | ||
| queue team-a Allocated (1000m + 2000m) over limit (2000m) | ||
| ``` | ||
|
|
||
| The pod keeps running at its current size; nothing is restarted or evicted. | ||
|
|
||
| ### Best-effort semantics | ||
|
|
||
| Admission is a guardrail, not a transactional guarantee: | ||
|
|
||
| - The webhook is registered with `failurePolicy: Ignore` — if the admission service is unavailable, resizes are admitted rather than blocked. | ||
| - Lookup failures (queue, podgroup) admit the resize and log an error. | ||
| - The check reads `Queue.status.allocated`, which trails actual scheduling decisions through the controller reconcile chain. A resize can therefore race concurrent activity on a nearly-full queue — another resize, or the scheduler placing new pods — and together exceed the limit. The reverse also holds: a recently freed queue may transiently over-deny a valid resize. The scheduler's own accounting stays correct either way and stops further allocation once over the limit. | ||
| - A resize issued while the same pod's previous resize is still being enacted is checked against the transient charge. In particular, a pending downsize is still charged until the kubelet enacts it, so a follow-up upsize within that window can reclaim the not-yet-released capacity and settle above the limit. | ||
|
|
||
| For workloads where the race above is unacceptable, see `blockUpsizeOnBoundedQueues` below. | ||
|
|
||
| ## Configuration | ||
|
|
||
| Resize admission is configured in the KAI `Config` resource: | ||
|
|
||
| ```yaml | ||
| apiVersion: kai.scheduler/v1 | ||
| kind: Config | ||
| metadata: | ||
| name: kai | ||
| spec: | ||
| admission: | ||
| inPlacePodResize: | ||
| validateQuota: true # default | ||
| blockUpsizeOnBoundedQueues: false # default | ||
| ``` | ||
|
|
||
| | Field | Default | Effect | | ||
| |---|---|---| | ||
| | `validateQuota` | `true` | Master switch. When `false`, the webhook admits all resizes without any checks, and `blockUpsizeOnBoundedQueues` is ignored. | | ||
| | `blockUpsizeOnBoundedQueues` | `false` | Strict mode: reject **any** upsize on a queue (or ancestor) that has a finite CPU or memory limit — regardless of free headroom — and, for non-preemptible workloads, on any queue with a finite quota. Closes the concurrent-resize race at the cost of disallowing upsizes on bounded queues entirely. | | ||
|
itsomri marked this conversation as resolved.
|
||
|
|
||
| ## Interaction with queue limits | ||
|
|
||
| `limit: -1` on a queue resource means unbounded; any other value (including `0`) is a hard bound enforced on resize. The same convention applies to `quota` for non-preemptible workloads. See [Queues](../queues/README.md) for how limits and quota are configured. | ||
|
|
||
| Because checks walk the full queue hierarchy, an upsize must fit under every ancestor's limit, not just the leaf queue's. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.