Skip to content

feat: InstanceType Inactive hold + aggregation phase filtering#32

Merged
thxCode merged 8 commits into
mainfrom
spec/instancetype-inactive-and-aggregation-phase
Jul 12, 2026
Merged

feat: InstanceType Inactive hold + aggregation phase filtering#32
thxCode merged 8 commits into
mainfrom
spec/instancetype-inactive-and-aggregation-phase

Conversation

@thxCode

@thxCode thxCode commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind enhancement
/kind api-change
/area worker
/area workergateway

What this PR does / why we need it:

Introduces an administrative Inactive switch on InstanceType plus a set of coordinated changes across the API types, the worker control plane, the admission webhook, and the workergateway aggregation:

  • Admin Inactive hold. InstanceTypeSpec.Inactive holds the backing Kueue ClusterQueue (StopPolicy=Hold): new admission is blocked, already-running workloads keep running (never HoldAndDrain for this path). Clearing the flag reactivates the queue (StopPolicy=None).
  • One-way sticky backfill. A queue stopped by any means (Hold or HoldAndDrain) is mirrored back into Spec.Inactive=true; the mirror never clears on None, so a pool that lost all its nodes (or was torn down and later recovered) stays Inactive until an admin explicitly clears it. Ownership is cleanly split — the InstanceType path only ever toggles Hold↔None; HoldAndDrain is owned solely by NodeQueueReconciler (teardown / no-flavors drain).
  • Distinct human status. InstanceType status reads Draining while a HoldAndDrain queue still holds reservations and Inactive once held/drained — derived from Spec.StopPolicy + reservation state.
  • Running instances survive an admin hold. InstanceReconciler reads the backing ClusterQueue's StopPolicy (not the InstanceType phase) before (re)creating a Pod: HoldAndDrain (a pool drain or a teardown that evicts admitted workloads) stops the Instance; Hold keeps running Pods; a ClusterQueue watch re-enqueues on a StopPolicy change so the stop stays level-based. This also closes a pre-existing gap where an evicted Pod under a draining queue was recreated and left stuck Pending.
  • DisplayName (≤64 runes) defaults to Product in the Default webhook (truncated to the field max).
  • CPU-manufacturer-agnostic descriptors. A CPU-only, awareness-off, non-acceleratable type no longer gets an arbitrary CPU's manufacturer/product/family stamped on it; the InstanceTypeFlavor catalog drops the matching Manufacturer=generic sentinel so the catalog and the derived type agree.
  • Aggregation Phase filtering. AggregatedInstanceTypeOnceMaxRequestCandidate records a per-candidate Phase; only Phase=="Active" candidates contribute to fleet OnceMaxRequest/Remaining, while inactive candidates remain listed. Cross-cluster grouping ignores Inactive/DisplayName.
  • API cleanup. InstanceSpec.Stop migrates from *bool to bool.
  • Chart uninstall. helm uninstall (and the gated cleanup hook) now also removes the worker-provisioned Secrets it leaves behind (<release>-worker-cert, gpustack-settings), deleted by fixed name so a co-located standalone GPUStack app's Secrets are untouched.

Which issue(s) this PR fixes:

None — spec-driven change.

Special notes for your reviewer:

  • Two reconcilers, one clean ownership split. InstanceTypeReconciler toggles only Hold↔None; NodeQueueReconciler owns HoldAndDrain. The forward direction (Inactive → StopPolicy) is evaluated before the one-way mirror; all writes are DeepEqual/value-guarded and idempotent, and the sync truth table is verified per-row for convergence without oscillation.
  • The sticky backfill is deliberate (memoryless, no observedGeneration): a recovered pool stays Inactive until an admin clears it — see the spec Non-Goals.
  • Instance stop keys on the queue StopPolicy, not the phase — the phase collapses Hold and a fully-drained HoldAndDrain to Inactive, and a fast drain skips a durable Draining, so the phase cannot drive the stop. Teardown (deletion) still stops immediately via the InstanceType's deletion timestamp.
  • Commits read as a per-task story: API fields → apistatus status mapping → Inactive↔StopPolicy sync → instance stop → CPU-agnostic descriptors + DisplayName → workergateway phase filtering, plus a standalone chart-uninstall fix.
  • Tested: make generate idempotent (clean tree), make lint, and make test (-race) all green; additionally validated end-to-end on a live Kubernetes cluster — an admin Hold keeps a running Instance's Pod, a pool drain stops the Instance (not recreate), and the InstanceType status reports Inactive/Draining.

Does this PR introduce a user-facing change?

`InstanceType` gains an administrative `Inactive` switch that holds its backing scheduling queue (blocks new admission while keeping already-running workloads) and a `DisplayName` field (defaults to `Product`); its status now distinguishes `Draining` from `Inactive`. Aggregated fleet capacity totals count only active instance types. `InstanceSpec.Stop` changes from `*bool` to `bool`.

thxCode added 4 commits July 12, 2026 17:08
… Stop to bool

- add InstanceTypeSpec.DisplayName (maxLength=64) and Inactive fields
- change InstanceSpec.Stop from *bool to bool; drop ptr wrappers at call sites
- regenerate protobuf, deepcopy, CRD, openapi and applyconfiguration

Task T0 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- map HoldAndDrain to Draining when reserved, else Inactive; Hold to Inactive
- add local clusterQueueHasReserved mirroring the node queue check
- pass the full ClusterQueue and simplify the draining check in computeStatus
- cover all four summary branches in a new table test

Task T1 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- add syncInactive converging Spec.Inactive and the queue StopPolicy per a
  forward-before-mirror truth table: Inactive drives the Hold<->None pair,
  a stopped queue backfills Inactive=true one-way, HoldAndDrain is never
  downgraded and never cleared on None (memoryless, non-oscillating)
- run the sync before computeStatus; skip the status refresh on a write
- update the reconciler doc to reflect the Hold<->None ownership
- cover all six truth-table rows plus post-convergence stability

Task T2 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
… policy

- stop a running instance when its type is gone or being deleted, or when its
  backing ClusterQueue is HoldAndDrain (a pool drain or a teardown that evicts
  admitted workloads); a Hold-Inactive type keeps its running Pods and a new
  Instance under it simply stays pending
- read the backing ClusterQueue StopPolicy directly rather than the InstanceType
  phase: the phase collapses Hold and drained-HoldAndDrain to Inactive and a fast
  drain skips a durable Draining, so it cannot drive the stop
- watch the ClusterQueue and re-enqueue the type's Instances on a StopPolicy
  change; narrow the InstanceType watch to fire only on the deletion (teardown)
  signal, keeping the stop level-based
- add InstanceTypePhaseDraining and refine the InstanceTypePhaseInactive doc
- cover hold-keeps-running, drain-stops, and a fully-drained HoldAndDrain
  stopping a pod-less instance; align the case-2 e2e and its drain-recycle
  reference to the StopPolicy-based log and watch

Task T3 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
Copilot AI review requested due to automatic review settings July 12, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enhances GPUStack Operator’s worker control plane and workergateway aggregation to support an administrative InstanceType.Spec.Inactive hold (mapped to Kueue ClusterQueue StopPolicy=Hold), adds DisplayName, refines draining vs inactive status derivation, and ensures aggregated capacity totals count only active instance types.

Changes:

  • Add Inactive + DisplayName to InstanceType, implement Inactive↔StopPolicy convergence, and derive Draining vs Inactive from queue stop policy + reservation state.
  • Update Instance stop behavior to key off the backing ClusterQueue StopPolicy (distinguishing admin Hold vs drain/teardown) and watch ClusterQueue for StopPolicy transitions.
  • Update workergateway aggregation to record per-candidate phase and exclude non-Active candidates from OnceMaxRequest/Remaining totals while retaining them in listings; plus grouping ignores per-cluster Inactive/DisplayName.

Reviewed changes

Copilot reviewed 23 out of 29 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/workergateway/service/types.go Adds candidate Phase field to aggregated instance type payload.
pkg/workergateway/service/helper.go Implements phase filtering in tier/item aggregation and normalizes grouping identity across clusters.
pkg/workergateway/service/helper_test.go Adds coverage for phase filtering, tier identity stability, and grouping behavior.
pkg/worker/webhooks/worker/instancetype.go Defaults/guards CPU descriptors for agnostic pools and defaults DisplayName (≤64 runes).
pkg/worker/webhooks/worker/instancetype_test.go Updates/extends webhook tests for CPU-agnostic behavior and DisplayName defaulting/truncation.
pkg/worker/webhooks/worker/instance.go Updates webhook logic for InstanceSpec.Stop migration to bool.
pkg/worker/webhooks/worker/instance_test.go Updates tests to reflect Stop as a bool.
pkg/worker/extensionapis/worker/instance_type_flavor.go Removes Manufacturer=generic sentinel for CPU-agnostic flavor spec.
pkg/worker/extensionapis/worker/instance_type_flavor_test.go Adjusts tests for absence of manufacturer sentinel.
pkg/worker/controllers/worker/nodequeue.go Prevents NodeQueue reconciler from reactivating admin Hold; only reactivates HoldAndDrain when drained empty.
pkg/worker/controllers/worker/nodequeue_test.go Adds test asserting admin Hold remains sticky across flavor return.
pkg/worker/controllers/worker/instancetype.go Adds syncInactive truth-table convergence and updates status computation via ClusterQueue summarizer.
pkg/worker/controllers/worker/instancetype_test.go Adds tests for Inactive↔StopPolicy truth table convergence and stability.
pkg/worker/controllers/worker/instance.go Stops instances on HoldAndDrain (not Hold), and adds ClusterQueue watch to re-enqueue on StopPolicy changes.
pkg/worker/controllers/worker/instance_test.go Updates tests to drive stop behavior via backing ClusterQueue StopPolicy.
pkg/worker/apistatus/cluster_queue.go Derives Draining/Inactive from StopPolicy and reservation presence; falls back to condition-based summary when not held.
pkg/worker/apistatus/cluster_queue_test.go Adds unit tests for StopPolicy-first phase derivation.
pkg/kubeclients/applyconfiguration/worker/v1alpha1/instancetypespec.go Adds apply-config builders for DisplayName and Inactive.
deploy/gpustack-operator/chart/files/cleanup.sh Extends uninstall cleanup to delete worker-provisioned Secrets.
api/worker/zz_generated.openapi.go Regenerates OpenAPI schema for new InstanceType fields.
api/worker/v1alpha1/zz_generated.deepcopy.go Regenerates deepcopy after InstanceSpec.Stop type change.
api/worker/v1alpha1/zz_generated.crds.go Regenerates CRDs for DisplayName/Inactive and Stop nullability removal.
api/worker/v1alpha1/instance.go Migrates InstanceSpec.Stop from *bool to bool.
api/worker/v1alpha1/instance_type.go Adds InstanceTypeSpec.DisplayName and InstanceTypeSpec.Inactive.
api/worker/v1alpha1/generated.proto Regenerates API proto for new InstanceTypeSpec fields.
api/worker/v1alpha1/generated.pb.go Regenerates protobuf Go bindings for API changes.
.claude/skills/gpustack-operator-e2e/references/drain-recycle.md Updates e2e reference docs for stop-on-drain behavior (StopPolicy-driven).
.claude/skills/gpustack-operator-e2e/cases/case-2.sh Updates e2e case assertions/log expectations to stop-on-drain behavior.
.claude/skills/_e2e-lib/scripts/teardown.sh Extends teardown to delete worker-provisioned Secrets.
Files not reviewed (6)
  • api/worker/v1alpha1/generated.pb.go: Generated file
  • api/worker/v1alpha1/generated.proto: Generated file
  • api/worker/v1alpha1/zz_generated.crds.go: Generated file
  • api/worker/v1alpha1/zz_generated.deepcopy.go: Generated file
  • api/worker/zz_generated.openapi.go: Generated file
  • pkg/kubeclients/applyconfiguration/worker/v1alpha1/instancetypespec.go: Generated file

Comment thread pkg/worker/webhooks/worker/instancetype.go
Comment thread deploy/gpustack-operator/chart/files/cleanup.sh Outdated
Comment thread .claude/skills/_e2e-lib/scripts/teardown.sh Outdated
Copilot AI review requested due to automatic review settings July 12, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 30 changed files in this pull request and generated 3 comments.

Files not reviewed (6)
  • api/worker/v1alpha1/generated.pb.go: Generated file
  • api/worker/v1alpha1/generated.proto: Generated file
  • api/worker/v1alpha1/zz_generated.crds.go: Generated file
  • api/worker/v1alpha1/zz_generated.deepcopy.go: Generated file
  • api/worker/zz_generated.openapi.go: Generated file
  • pkg/kubeclients/applyconfiguration/worker/v1alpha1/instancetypespec.go: Generated file

Comment thread pkg/worker/webhooks/worker/instancetype.go
Comment thread deploy/gpustack-operator/chart/files/cleanup.sh Outdated
Comment thread .claude/skills/_e2e-lib/scripts/teardown.sh Outdated
thxCode added 2 commits July 12, 2026 22:52
- Default webhook: clear manufacturer/product/family and skip enrichment for
  a CPU-manufacturer-agnostic type (awareness off and not acceleratable), so
  no arbitrary flavor identity is stamped onto the collapsed pool
- Default webhook: default DisplayName to Product (preserving an admin value);
  the agnostic guard path leaves it empty
- flavor catalog: drop the Manufacturer=generic sentinel from the CPU-agnostic
  "generic" row, aligning it with the derived InstanceType
- rewrite the awareness-off webhook test and add DisplayName + empty-manufacturer cases

Task T4 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- add Phase to AggregatedInstanceTypeOnceMaxRequestCandidate, populated at
  every candidate-construction site (Next + the three Handle sites)
- tier Recompute counts only Active candidates toward OnceMaxRequest/Remaining;
  non-Active candidates stay listed but contribute zero and never seed
- item Recompute skips all-zero (e.g. all-inactive) tiers from winner seeding
  so they cannot mask a fully-sliced active tier tying them at primary zero
- recompute every tier before sorting/aggregation on all paths (batch Result
  and the streaming cross-tier/new-tier Handle paths) so a non-Active candidate
  never leaks raw capacity nor mis-orders tiers by an unfiltered seed
- normalize the cross-cluster grouping key (zero Inactive/DisplayName) for the
  itemIndexer and the Handle spec comparison; keep the first-seen DisplayName
- match Handle tier identity on the stable Candidates[0].Accelerator.OnceMaxRequest

Task T5 of instancetype-inactive-and-aggregation-phase.

Signed-off-by: thxCode <thxcode0824@gmail.com>
Copilot AI review requested due to automatic review settings July 12, 2026 14:54
@thxCode thxCode force-pushed the spec/instancetype-inactive-and-aggregation-phase branch from 474b7ab to af70052 Compare July 12, 2026 14:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 30 changed files in this pull request and generated 1 comment.

Files not reviewed (6)
  • api/worker/v1alpha1/generated.pb.go: Generated file
  • api/worker/v1alpha1/generated.proto: Generated file
  • api/worker/v1alpha1/zz_generated.crds.go: Generated file
  • api/worker/v1alpha1/zz_generated.deepcopy.go: Generated file
  • api/worker/zz_generated.openapi.go: Generated file
  • pkg/kubeclients/applyconfiguration/worker/v1alpha1/instancetypespec.go: Generated file

Comment thread deploy/gpustack-operator/chart/files/cleanup.sh Outdated
thxCode added 2 commits July 12, 2026 23:46
- The worker's webhook serving cert (<release>-worker-cert) and the delegated
  editable-settings store (gpustack-settings) are provisioned at runtime, not by
  helm templating, so `helm uninstall` left them behind in the namespace.
- Delete both by fixed name in the post-delete cleanup hook, and mirror the same
  step into the e2e teardown script (its documented single source of truth).
  Delete by name, never a label sweep, so a co-located standalone GPUStack app's
  own secrets are untouched.

Found while tearing down the live e2e for the instancetype-inactive-and-aggregation-phase spec.

Signed-off-by: thxCode <thxcode0824@gmail.com>
…hip with the shipped model

- rewrite the running-instance stop paragraph: the stop reads the backing
  ClusterQueue StopPolicy (HoldAndDrain, or the InstanceType deleted/gone), an
  admin Hold keeps running Pods, and a ClusterQueue watch drives it while the
  InstanceType watch is narrowed to the deletion signal
- record the InstanceTypeReconciler's admin Inactive<->StopPolicy sync and its
  one-way sticky backfill, and split StopPolicy ownership across the two
  reconcilers (Hold<->None vs HoldAndDrain) in both the prose and the diagram

Signed-off-by: thxCode <thxcode0824@gmail.com>
Copilot AI review requested due to automatic review settings July 12, 2026 15:48
@thxCode thxCode force-pushed the spec/instancetype-inactive-and-aggregation-phase branch from af70052 to 405d713 Compare July 12, 2026 15:48
@thxCode thxCode merged commit d04c565 into main Jul 12, 2026
13 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 31 changed files in this pull request and generated 3 comments.

Files not reviewed (6)
  • api/worker/v1alpha1/generated.pb.go: Generated file
  • api/worker/v1alpha1/generated.proto: Generated file
  • api/worker/v1alpha1/zz_generated.crds.go: Generated file
  • api/worker/v1alpha1/zz_generated.deepcopy.go: Generated file
  • api/worker/zz_generated.openapi.go: Generated file
  • pkg/kubeclients/applyconfiguration/worker/v1alpha1/instancetypespec.go: Generated file

Comment on lines +528 to 531
if tier.Candidates[0].Accelerator.OnceMaxRequest.Equal(candidate.Accelerator.OnceMaxRequest) {
// If the once max request has not changed, update the candidate in place.
tier.Candidates[index[2]] = *candidate

Comment on lines 611 to 615
candidate := &AggregatedInstanceTypeOnceMaxRequestCandidate{
Cluster: evt.Cluster,
Name: instType.Name,
Phase: instType.Status.Phase,
Accelerator: instType.Status.Accelerator,
Comment on lines +349 to 352
draining := ptr.Deref(cq.Spec.StopPolicy, kueue.None) == kueue.HoldAndDrain

var st workercore.InstanceTypeStatus
if !draining {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants