Policy Algorithms
This page documents the controller policy algorithms implemented in pkg/controller/policy/.
Use this page after:
Classification Input
Policy demand classification is derived from the joulie.io/workload-class pod annotation:
performance: pod carriesjoulie.io/workload-class: performance.standard(default): no annotation orjoulie.io/workload-class: standard.
Shared Reconcile Flow
Each reconcile tick:
- Select eligible nodes from
NODE_SELECTOR, excluding reserved and unschedulable nodes. - Build a hardware view from
NodeHardwarewhen available, otherwise from node labels/inventory fallback. - Build a desired plan with the selected policy.
- Apply downgrade guard (sets
NodeTwin.status.schedulableClasstodrainingwhile blocking pods still run). - Write
NodeTwin.specand update thejoulie.io/power-profilenode label.
In other words, static_partition and queue_aware_v1 decide how many nodes stay in performance, and the per-family split below decides which nodes those are.
Performance Node Selection
static_partition and queue_aware_v1 compute a performance slot count hp_count and pass it to policy.PerformanceSet:
- Group the eligible nodes into hardware families (
policy.NodeFamily):gpu:<model>when the node has GPUs, otherwisecpu:<model>. - Raise
hp_countto the number of families and cap it at the number of nodes. - Give every family one slot. Each further slot goes to the family with the largest
size / (2 * slots + 1), ties to the smaller family key (the Sainte-Lague divisor method). A family never gets more slots than it has nodes. - Within a family, fill the slots with nodes whose current
joulie.io/power-profilelabel isperformancefirst, then by node name.
Properties:
- every family keeps performance capacity in proportion to its size, and at least one node,
- house monotone: when
hp_countchanges by one, exactly one node changes profile, - a reconcile at constant demand moves no node, because nodes already in
performanceare kept first, - the result does not depend on the order of the input node list.
Example: the exp02 5k inventory (experiments/02-heterogeneous-benchmark/configs/cluster-nodes-5k.yaml, 5000 nodes in 8 families) with 1000 performance slots (STATIC_HP_FRAC=0.2) keeps a fifth of every family in performance:
| Family | Nodes | Performance |
|---|---|---|
| h100-nvl | 1450 | 290 |
| h100-sxm | 730 | 146 |
| l40s | 850 | 170 |
| mi300x | 240 | 48 |
| w7900 | 730 | 146 |
| cpu-highcore | 250 | 50 |
| cpu-highfreq | 250 | 50 |
| cpu-intensive | 500 | 100 |
The standalone simulator calls the same policy.PerformanceSet.
The controller manager exports the split after the downgrade guard as joulie_policy_family_nodes{family} and joulie_policy_family_performance_nodes{family}, so a draining node counts as not performance.
static_partition
Goal: deterministic fixed HP/LP split.
Inputs:
N: number of eligible nodes.STATIC_HP_FRAC: target fraction of high-performance nodes.
Algorithm:
hp_count = round(N * STATIC_HP_FRAC).- Pick the performance nodes with the per-family split, which raises
hp_countto the number of families and caps it atN. - Remaining nodes ->
eco.
Properties:
- deterministic,
- stable over time unless node set changes.
- keeps about
STATIC_HP_FRACof every hardware family inperformance, and at least one node per family.
This policy is exercised in the CPU-Only Benchmark and Heterogeneous GPU Cluster Benchmark.
queue_aware_v1
Goal: adapt HP count to current performance-only pressure.
Inputs:
N: number of eligible nodes.P: count of performance-sensitive pods cluster-wide that are running or pending, so performance pods still waiting for a node raise the demand. Every Pending performance pod counts, including one that more performance nodes would not help (an unsatisfiable constraint, an image pull);QUEUE_HP_MAXbounds the effect. The downgrade guard counts running pods only.QUEUE_HP_BASE_FRACQUEUE_HP_MINQUEUE_HP_MAXQUEUE_PERF_PER_HP_NODE
Algorithm:
base = round(N * QUEUE_HP_BASE_FRAC).need = ceil(P / QUEUE_PERF_PER_HP_NODE).hp_count = max(base, need).- Clamp
hp_countto[QUEUE_HP_MIN, QUEUE_HP_MAX]. - Pick the performance nodes with the per-family split, which raises
hp_countto the number of families and caps it atN. - Remaining nodes ->
eco.
Properties:
- deterministic for a fixed
(N, P)and the current profile labels, - monotonic in pressure
P, - bounded by min/max limits, except that every family keeps at least one performance node,
- heterogeneous-aware because slots follow family size, and a change of
hp_countby one moves exactly one node.
This policy is exercised in the CPU-Only Benchmark and Heterogeneous GPU Cluster Benchmark.
rule_swap_v1 (debug policy)
Goal: force visible state transitions for debugging.
Algorithm:
- Compute phase from wall-clock and
RECONCILE_INTERVAL. - Alternate which of the first two eligible nodes, in node-name order, is assigned
eco. - Others remain
performance.
This policy is intended for debugging only, not as default production behavior.
Downgrade Guard
When the planned profile of a node is eco:
- Count active performance pods on that node.
- If count > 0:
- keep desired profile as
eco, - set
NodeTwin.status.schedulableClasstodraining, - write the performance caps in
NodeTwin.specuntil those pods finish, so they are not throttled; this holds for a node leavingperformanceand for one already inecothat runs a performance pod, - record transition as deferred in controller manager FSM/metrics.
- keep desired profile as
- If count == 0:
- keep desired profile
eco, - set
NodeTwin.status.schedulableClasstoeco.
- keep desired profile
The scheduler extender reads schedulableClass and filters draining nodes out for performance pods, as it does eco nodes, so no new performance work lands on a node during the transition. Standard pods are not filtered.