Skip to content

Experiment/reservation race pending - #1

Draft
cgetzen wants to merge 7 commits into
mainfrom
experiment/reservation-race-pending
Draft

Experiment/reservation race pending#1
cgetzen wants to merge 7 commits into
mainfrom
experiment/reservation-race-pending

Conversation

@cgetzen

@cgetzen cgetzen commented Aug 11, 2026

Copy link
Copy Markdown

Description

Related Issues

Fixes #

Checklist

Note: Ensure your PR title follows the Conventional Commits format (e.g., feat(scheduler): add new feature)

  • Self-reviewed
  • Added/updated tests (if needed)
  • Updated documentation (if needed)
  • Added a changelog fragment via make changelog (or applied the skip-changelog label). Do not edit CHANGELOG.md directly — pending fragments are folded into it at release time.

Breaking Changes

Additional Notes

cgetzen and others added 7 commits August 10, 2026 10:15
Meshy fork: let a custom/external scheduler (e.g. slurm-bridge) place the
whole-GPU reservation pod so Slurm accounts the whole GPU, while KAI packs
fractions onto it. Validated e2e on slurm-staging (Tesla T4): reservation
placed by slurm-bridge as a DRA extended-resource request, 2x0.5 fractions
shared, no double-booking.

- Reservation pod can request GPUs via the DRA extended resource
  (deviceclass.resource.kubernetes.io/gpu.nvidia.com) instead of the classic
  nvidia.com/gpu device-plugin resource.
- New Config knobs threaded operator -> binder args:
  reservationSchedulerName, reservationGpuResourceName, reservationPodAnnotations.
- External mode: create the reservation pod and wait for the external scheduler
  to place it (follow-the-reservation), with a node-divergence guard for the v1
  static single-GPU-node pool.
- Wire --resource-reservation-allocation-timeout through options/app.
- Relax the HasDRAGPUs fractional guard to reject only DRA-ONLY nodes; a node
  that also advertises device-plugin GPUs (+ nvidia.com/gpu.memory) has the
  signals KAI's sharing accounting needs, so fractions work there even though
  the reservation is held via DRA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix A for the reservation-race work: the root cause of the frac-pod
create->pend->reap churn on a "both" node (device plugin AND DRA exposing the
same physical GPUs).

- populateDRAGPUs added DRA ResourceSlice GPUs on TOP of the device-plugin
  allocatable, double-counting one physical T4 as two. The extra phantom idle
  GPU made the scheduler keep opening new whole-GPU groups for an
  already-unschedulable fraction, spawning reservation pods that slurm-bridge
  could never place. Now add only max(0, DRA - device-plugin): the node's true
  GPU count is max(device-plugin, DRA), since both describe the same hardware.
- Fix the "fractional not supported on DRA-only nodes" guard: it tested
  AllocatableVector[GPU] <= 0, but AddDRAGPUs merges DRA GPUs into that vector,
  so it was never <= 0 on a DRA node and the guard silently no-op'd (broke the
  upstream TestPredicateByNodeResourcesType_SharedGPU_DRANode). Capture the
  device-plugin GPU count at construction (new NodeInfo.DevicePluginGPUs, set
  before AddDRAGPUs) and gate on that instead.

Tests: both-node + excess-DRA cases in cluster_info; both-node accept case in
node_info; the previously-broken DRA-only reject cases pass again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design-2 foundation for the reservation race: today a reservation pod that
does not allocate within --resource-reservation-allocation-timeout (40s) is
deleted and the BindRequest fails+rolls back, so an externally-scheduled
reservation (slurm-bridge) that is merely queued in Slurm gets churned.

Now, when the reservation pod is still Pending at timeout (vs failed/gone), the
binder PARKS it:
- waitForGPUReservationPodAllocation returns a distinct pendingGpuIndicator;
  timeoutAllocationIndicator inspects the pod phase to decide park vs reap.
- createGPUReservationPodAndGetIndex does not delete a parked reservation and
  returns ErrReservationPending (wrapped, so errors.Is works up the stack).
- The BindRequest reconciler treats ErrReservationPending as "retry later": no
  Rollback (keeps the fraction's GPU-group binding), no terminal phase, no
  failed-attempt increment; just RequeueAfter to poll for placement.

Teardown is already handled: a parked reservation is protected from GC while its
BindRequest is non-terminal (hasActiveBindRequestsForGpuGroup); once the fraction
binds elsewhere (packs onto a freed GPU) and the BindRequest goes terminal, the
reservation sync deletes it.

Unit test covers the park-vs-reap decision. Note: this parks reservations the
scheduler already proposed; making the scheduler reserve-ahead for an
unschedulable fraction on a full node (so a reservation is enqueued to Slurm
speculatively) is the remaining scheduler-side piece.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… race)

Design-2 completion: a decoupled reconciler that reserves a whole GPU ahead for
a fraction that can't yet pack, WITHOUT the KAI scheduler having to decide
cluster fullness. It composes with the parking primitive and Fix A's pack-on-free
rather than modifying core scheduler fit logic.

For a GPU-fraction pod that has been Pending + unschedulable past a short delay:
- EnsureReservation creates ONE parked whole-GPU reservation (external/slurm-bridge,
  no nodeName) and labels the fraction with the reservation's stable GPU group, so
  the existing reservation sync keeps it alive and reaps it once the fraction
  settles elsewhere.
- The fraction is deliberately left Pending, so the KAI scheduler keeps trying to
  pack it onto a GPU that frees up. Whichever wins the race:
    * scheduler packs it onto a freed reserved GPU -> its group label flips ->
      reserve-ahead reservation orphaned -> torn down by the sync;
    * reserve-ahead reservation gets placed (Slurm preempts/queues) ->
      ReservationPlacement reports ready -> we create the BindRequest to bind the
      fraction onto it.
- BindRequests are named per-pod, so a scheduler/reconciler race resolves to a
  single winner (AlreadyExists -> back off).

New reservation-service methods: EnsureReservation (node-less external create) and
ReservationPlacement (ready + node). Enabled only in external-reservation mode.
Wired into the binder manager. Unit tests cover group stability, the wait-before-
reserve delay, ensure+label, and bind-on-placement. No core scheduler changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…correctness)

On-cluster testing (2-GPU staging) surfaced a bug: after the scheduler packed a
parked fraction onto a freed GPU, the reserve-ahead reconciler kept running (its
cache still saw the pod Pending) and re-labeled the fraction back to the
reserve-ahead group, overwriting the binder's real group. That (a) corrupted the
scheduler's GPU-group accounting and (b) leaked the parked reservation (its group
still looked referenced, so the sync never reaped it).

Fix: a BindRequest for the fraction means its placement is decided, so the
reconciler stops touching it:
- BindRequest binds onto our reserve-ahead group  -> reservation won the race; keep it.
- BindRequest binds onto a different group (pack won) -> repoint the pod's group label
  to the real bound group and tear down the now-orphaned reserve-ahead reservation.
The ensure/label path only runs while NO BindRequest exists.

Validated on a 2x T4 cluster: multi-GPU packing; reserve-ahead parks one
reservation (no churn); pack-wins race repoints the label + reaps the reservation;
whole-GPU-drain converges to one reservation per GPU with no leak. Slurm shows
exactly one gres/gpu job per node; fractions share the correct physical GPU; zero
device-plugin GPU consumers (pure DRA, no double-book). New unit tests cover the
pack-won teardown and reservation-won keep paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…deleted

Deleting a reservation ("capacity") pod frees the whole GPU in Slurm immediately
(slurm-bridge cancels the GRES job on pod termination) while the fraction
workloads keep running on it — a silent double-booking. Neither a finalizer nor a
grace period can keep the GPU held (kubelet terminates the container on
deletionTimestamp regardless of finalizers; k8s finalizers are unordered so they
can't gate slurm-bridge's cleanup). So instead, cascade the deletion downward.

- Externally-scheduled reservation pods now carry ReservationCascadeFinalizer.
- New ReservationCascadeReconciler watches reservation pods: on deletion it deletes
  the GPU-fraction pods sharing the reservation's GPU group, and HOLDS the finalizer
  until they are actually gone — so the reservation stays Terminating until the
  workloads have drained, then is released. This is the "wait for the other pods to
  terminate" behavior, implemented via the finalizer (terminationGracePeriodSeconds
  only governs a pod's own containers, not others).

Enabled only in external-reservation mode (where the binder stamps the finalizer),
so vanilla-mode reservation pods are never stranded by a finalizer nothing removes.
Unit tests cover cascade+wait, immediate-release when no fractions, and disabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al churn

The reservation-cascade controller fired on ANY reservation-pod deletion,
including the reservation service's own internal deletes (reap-on-timeout, the
node-divergence guard, and syncForPods orphan cleanup). During the bind window
the sync can transiently see a group as having "no fraction pod" (the fraction's
gpu-group label isn't visible in its informer yet, and the BindRequest is already
gone so hasActiveBindRequestsForGpuGroup doesn't protect) and delete the
reservation — which the cascade then amplified into deleting the just-bound,
healthy fraction workload. Reproduced on-cluster: a fraction pod deleted ~15s
after binding.

Fix:
- Internal deleteReservationPod strips the cascade finalizer before deleting, so
  a system-initiated deletion does NOT cascade. Only an external delete (which
  leaves the finalizer intact) triggers the cascade — the intended semantic.
- The cascade controller no longer re-adds the finalizer on non-deleting
  reservations (the binder stamps it at creation); re-adding would race-undo the
  strip above.

Net: internal reservation churn is back to non-lethal (at worst a reservation
flap), while `kubectl delete` of a capacity pod still cascades to its fractions.
Unit test covers the finalizer strip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant