|
| 1 | +# Guaranteed minimum runtime before preemptions and reclaims |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document proposes a new feature called "reclaim-min-runtime" and "preempt-min-runtime" that provides configurable guarantees for workload runtime before preemption or reclaim can occur. This feature enables administrators to define minimum runtime guarantees at various levels of the resource hierarchy: node pool, nth level queue and leaf-queue. |
| 6 | + |
| 7 | +## Motivation |
| 8 | + |
| 9 | +Unrestrained preemption can lead to resource thrashing, where workloads are repeatedly preempted before making meaningful progress. The min-runtime feature addresses this issue by providing configurable minimum runtime guarantees that can be set by either cluster operators or sometimes within parts of the queue to provide guarantees about minimal useful work. |
| 10 | + |
| 11 | +## Detailed Design |
| 12 | + |
| 13 | +### Reclaim Min Runtime Configuration |
| 14 | + |
| 15 | +The reclaim-min-runtime parameter can be configured with the following values: |
| 16 | + |
| 17 | +- **0 (default)**: Workloads are always preemptible via reclaims |
| 18 | +- **Positive value**: Minimum guaranteed runtime before preemption via reclaims |
| 19 | + |
| 20 | +### Preempt Min Runtime Configuration |
| 21 | + |
| 22 | +In addition to protecting workloads from being preempted too early with reclaim-min-runtime, we also introduce preempt-min-runtime to ensure that in-queue preemptions are also protected with min-runtime. |
| 23 | + |
| 24 | +The preempt-min-runtime parameter can be configured with the following values: |
| 25 | + |
| 26 | +- **0 (default)**: Workloads can always be preempted by others (subject to reclaim-min-runtime constraints) in the same queue |
| 27 | +- **Positive value**: Minimum guaranteed runtime before in-queue preemption |
| 28 | + |
| 29 | +### Configuration Hierarchy |
| 30 | + |
| 31 | +The configuration follows a hierarchical override structure: |
| 32 | + |
| 33 | +1. **Node Pool Level**: Base configuration that applies to all reclaims/preemptions if not overridden by a queue. Default will be set to 0 which preserves existing behaviors to always reclaim/preempt. |
| 34 | +2. **Queue Level**: Overrides node pool configuration for reclaims/preemptions, can be further overridden by a child queue. Default will be set to unassigned, causing the node pool level value to be used. |
| 35 | + |
| 36 | +### Resolving the applicable min-runtime for reclaims and preemptions |
| 37 | + |
| 38 | +#### Reclaims (preemptor and preemptee are in different queues) |
| 39 | +1. Resolve the lowest common ancestor (LCA) between the leaf-queues of preemptor and preemptee. |
| 40 | +2. Walk 1 step down to the child of the LCA that is an ancestor to the preemptee's leaf queue (or is the leaf queue). |
| 41 | +3. Use the reclaim-min-runtime from this queue, if it is set. Otherwise move back up towards root of tree and select the first available queue-level override, or default to the node pool-level configuration value. |
| 42 | + |
| 43 | +The idea around the algorithm here is to isolate settings of min-runtime in the queue tree to only affect siblings in reclaim scenarios, and for the potential to distribute the administration of these values in the queue tree (such as giving a user access to change parts of the tree). |
| 44 | +As a follow-up, we could also provide a setting to disable this and always use the leaf-tree resolved value in all cases. This could be favorable in a scenario where all min-runtimes in the queue tree are managed by one entity. |
| 45 | + |
| 46 | +##### Example |
| 47 | +```mermaid |
| 48 | +graph TD |
| 49 | + A[Queue A] --> B[Queue B<br/>600s] |
| 50 | + B --> C[Queue C] |
| 51 | + B --> D[Queue D<br/>60s] |
| 52 | + C --> leaf1[Queue leaf1<br/>0s] |
| 53 | + C --> leaf2[Queue leaf2<br/>180s] |
| 54 | + D --> leaf3[Queue leaf3] |
| 55 | +``` |
| 56 | + |
| 57 | +1. A preemptor in leaf-queue `root.A.B.C.leaf1` and a preemptee in leaf-queue `root.A.B.D.leaf3` will use the min-runtime resolved for `root.A.B.D` (60s). |
| 58 | + |
| 59 | +2. A preemptor in leaf-queue `root.A.B.C.leaf1` and a preemptee in leaf-queue `root.A.B.C.leaf2` will use the min-runtime resolved for `root.A.B.C.leaf2` (180s). |
| 60 | + |
| 61 | +3. A preemptor in leaf-queue `root.A.B.D.leaf3` and a preemptee in leaf-queue `root.A.B.C.leaf1` will use the min-runtime resolved for `root.A.B.C` (600s inherited from ancestor `root.A.B`). |
| 62 | + |
| 63 | +#### Preemptions (preemptor and preemptee are within the same leaf-queue) |
| 64 | +Starting from the leaf-queue, walk the tree until the first defined preempt-min-runtime is set and use that. |
| 65 | + |
| 66 | +##### Example |
| 67 | +```mermaid |
| 68 | +graph TD |
| 69 | + A[Queue A] --> B[Queue B<br/>600s] |
| 70 | + B --> C[Queue C] |
| 71 | + C --> leaf1[Queue leaf1<br/>300s] |
| 72 | + C --> leaf2[Queue leaf2] |
| 73 | +``` |
| 74 | + |
| 75 | +1. `root.A.B` has preempt-min-runtime: 600, `root.A.B.C.leaf1` has preempt-min-runtime: 300. Workloads in leaf1 will have preempt-min-runtime: 300. |
| 76 | + |
| 77 | +2. `root.A.B` has preempt-min-runtime: 600, `root.A.B.C.leaf2` has preempt-min-runtime unset. Workloads in leaf2 will have preempt-min-runtime: 600. |
| 78 | + |
| 79 | + |
| 80 | +## Development |
| 81 | + |
| 82 | +### Phase 1 |
| 83 | + |
| 84 | +Add startTime to PodGroup by mimicking how staleTimestamp is set today: |
| 85 | +https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/cache/status_updater/default_status_updater.go#L149-L154 |
| 86 | + |
| 87 | +This will be a readable annotation that is set to current time when the workload has been successfully allocated. |
| 88 | + |
| 89 | +For scheduling purposes, the readable timestamp is converted to a unix timestamp when pods are snapshotted, using https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/api/podgroup_info/job_info.go#L81 |
| 90 | + |
| 91 | +For a more advanced scenario, we could also make use of scheduling conditions, but have left that out of the design proposal for now. |
| 92 | + |
| 93 | +### Phase 2 |
| 94 | + |
| 95 | +Prepare https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/framework/session_plugins.go to expose `IsPreemptible(actionType, preemptor, preemptee) bool` extension function. |
| 96 | + |
| 97 | +For the new function we will do boolean AND between the results of each plugin returning the values, and use the result of that to determine if the workload is preemptible at all. |
| 98 | + |
| 99 | +`IsPreemptible()` will be called in each action's victim selection filters, and will be called only AFTER a workload has been considered eligible based on the fundamental filters of "reclaims" and "preemptible" (such as preemptible only being relevant for in-queue workloads). |
| 100 | + |
| 101 | +https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/actions/preempt/preempt.go#L105-L134 |
| 102 | + |
| 103 | +https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/actions/reclaim/reclaim.go#L154-L158 |
| 104 | + |
| 105 | + |
| 106 | +Secondly, because elastic workloads can always be partially preempted, we will also expose another plugin hook that allows plugins to inject new scenario filters to be used here: |
| 107 | +https://github.com/NVIDIA/KAI-Scheduler/blob/eb01078bf26f8f85ea20d44ba3b15912dae95e55/pkg/scheduler/actions/common/solvers/pod_scenario_builder.go#L100-L114 |
| 108 | + |
| 109 | +`GetAccumulatedScenarioFilters(session *framework.Session, actionType ActionType, pendingJob *podgroup_info.PodGroupInfo)` will return a list of instances matching `accumulated_scenario_filters.Interface`. In `session_plugins.go`, the result from all plugins will be aggregated into a resulting slice, that is then returned to NewPodAccumulatedScenarioBuilder and appended to the list of scenarioFilters: |
| 110 | +https://github.com/NVIDIA/KAI-Scheduler/blob/eb01078bf26f8f85ea20d44ba3b15912dae95e55/pkg/scheduler/actions/common/solvers/pod_scenario_builder.go#L47-L51 |
| 111 | + |
| 112 | +To get correct data about the action for the filter, we will propagate `actionType` down into the scenario builder so that the filter can be constructed based on the type of action taken. |
| 113 | + |
| 114 | + |
| 115 | +### Phase 3 |
| 116 | + |
| 117 | +Implement configuration options for (preempt|reclaim)-min-runtime in node pool and queue configurations. |
| 118 | + |
| 119 | +For node pool level, `pkg/scheduler/conf/scheduler_conf.go` seems like the appropriate place, in `SchedulerConfiguration`. |
| 120 | +https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/scheduler/conf/scheduler_conf.go#L18-L43 |
| 121 | + |
| 122 | + |
| 123 | +Since queues are defined as CRDs, the extra values will have to be implemented in `pkg/apis/scheduling/v2/queue_types.go` under `QueueSpec`. |
| 124 | +https://github.com/NVIDIA/KAI-Scheduler/blob/420efcc17b770f30ca5b899bc3ca8969e352970a/pkg/apis/scheduling/v2/queue_types.go#L26-L49 |
| 125 | + |
| 126 | +If CRD allows it, we will use `time.Duration` to describe these values, otherwise integer with seconds as value. |
| 127 | + |
| 128 | +It has been suggested to create a new v3alpha1 for these changes. |
| 129 | + |
| 130 | +### Phase 4 |
| 131 | + |
| 132 | +Implement min-runtime plugin for the scheduler that extends `IsPreemptible()`, which will be used to filter out workloads eligible for preemption when scheduler tries to take these actions. We will also extend `GetAccumulatedScenarioFilters()` to validate and filter out scenarios that attempt to preempt elastic workloads beyond MinAvailable when there is min-runtime left. |
| 133 | + |
| 134 | +We will evaluate workloads in `IsPreemptible()` as follows: |
| 135 | + |
| 136 | + 1. If MinAvailable is set, always return true, as elastic workloads are handled by scenario filter instead. |
| 137 | + 2. Resolve the correct min-runtime given actionType, preemptor and preemptee. |
| 138 | + 3. If currentTime > startTime + resolved min-runtime, return true. |
| 139 | + 4. Else false. |
| 140 | + |
| 141 | +To handle elastic workload preemptability (which our plugin will always consider preemptible), we would do as follows: |
| 142 | + |
| 143 | +When the solver creates the scenario builder, `GetAccumulatedScenarioFilters()` will call our plugin which returns a `ElasticMinRuntimeFilter` that is defined within the min-runtime plugin and adds it to the list of scenario filters. |
| 144 | + |
| 145 | +When the filter is called as a scenario is generated, it will look at `scenario.victimJobsTaskGroups` and `potentialVictimsTasks` together with `pendingJob`. |
| 146 | +If any of the `victmJobsTaskGroups` are an elastic workload with min-runtime left with regards to `pendingJob` (using the same min-runtime resolver mentioned earlier), the scenario will be considered invalid if `recordedVictimTasks` and `potentialVictimTasks` would bring the elastic workload below MinAvailable pods. |
0 commit comments