You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/developer/designs/numa-topology/README.md
+42-21Lines changed: 42 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -601,38 +601,59 @@ regardless; Appendix A is the in-plugin fallback if the assumption proves insuff
601
601
602
602
## v2: Optimization & scoring
603
603
604
-
v1 decides *feasibility* — can this node host the pod without a `TopologyAffinityError`. v2
605
-
decides *which feasible node is best*, via a node score (`AddNodeOrderFn`, a new band in
606
-
`scores/scores.go`). It reuses v1's evaluators and per-zone model unchanged: it only **ranks**
607
-
nodes, never alters the admit decision.
604
+
v2 adds scoring based on NUMA placement: nodes that can fit a NUMA-sensitive task in fewer zones
605
+
are ranked higher. Nodes that can't admit the task are ranked lower. This enables us to support
606
+
`best-effort` mode better.
607
+
608
+
The upstream scheduler numa plugin supports different scoring strategies - `LeastNUMANodes`,
609
+
`BalancedAllocation`, `LeastAllocated` and `MostAllocated` - the last three are only relevant to
610
+
`single-numa-node` mode. `LeastNUMANodes` seems the most relevant for our use cases, but we can
611
+
support more modes, and welcome community feedback on this.
608
612
609
613
### What scoring adds
610
614
611
615
-**Optimize `best-effort` performance.** On a `best-effort` node the kubelet never rejects — it
612
616
silently runs the pod *unaligned* when it can't fit a NUMA node, costing throughput. v1 does
613
617
nothing for `best-effort` (there is no admission error to prevent). v2 **scores**`best-effort`
614
-
nodes by whether the pod's resources *can* be aligned there, steering it toward a node where
615
-
the kubelet's best-effort alignment will actually succeed — turning a silent performance loss
616
-
into a good placement. This is the primary motivation for v2.
617
-
-**Prefer tighter, less-fragmented fit** on feasible `single-numa-node` / `restricted` nodes, so
618
-
later pods still find aligned room, and multi-NUMA pods span the fewest zones.
619
-
620
-
### Scoring strategies
621
-
622
-
Reusing the upstream NodeResourceTopology scoring vocabulary, computed over the plugin's per-zone
623
-
model:
624
-
625
-
-**LeastNUMANodes** (policy-agnostic) — prefer nodes where the pod spans the fewest NUMA nodes
626
-
(ideally one). This is the core `best-effort` steering and the multi-NUMA-span minimizer.
627
-
-**LeastAllocated / MostAllocated / BalancedAllocation** — spread vs. bin-pack vs. balance
628
-
per-zone utilization, for fragmentation control on the aligned policies; selectable via config.
618
+
nodes by how few zones the pod's resources *can* be aligned to there, steering it toward a node
619
+
where the kubelet's best-effort alignment will actually succeed. This is the primary motivation for v2.
620
+
-**Prefer tighter, fewer-zone fit** on feasible `restricted` nodes, where the kubelet forces the
621
+
pod to span its preferred width `w` (which differs per node by per-zone `Allocatable`): rank
622
+
nodes by `w` so a pod that aligns to one zone on node A beats spanning two on node B.
623
+
-**Sink infeasible nodes so the predicate short-circuits.**`OrderedNodesByTask` scores *every*
624
+
candidate node — including ones the predicate will reject — and the action then runs `FittingNode`
625
+
(the predicate) lazily **in score order**, stopping at the first fit. So ranking a node the
626
+
kubelet can't NUMA-align *below* one it can makes the predicate hit a feasible node first and skip
627
+
evaluating the infeasible ones. This applies to **`single-numa-node`** too: its feasible span is
628
+
always 1, but feasibility itself is a ranking signal, so scoring is *not* a no-op there — it
629
+
front-loads the alignable nodes. (Correctness still rests on the predicate; the score only reorders.)
630
+
631
+
### The fewest-zones span, per policy
632
+
633
+
Scoring routes every candidate node through one reusable function, `alignmentSpan(task, node) →
634
+
(zones int, aligned bool)`, and the score is a function of **both** outputs: `aligned=false` sinks
635
+
the node (worst score), and among aligned nodes fewer `zones` scores higher.
636
+
637
+
| Policy |`alignmentSpan` when aligned |`aligned=false` when | Predicate outcome |
638
+
| --- | --- | --- | --- |
639
+
|`single-numa-node`|`1`| no single zone fits by `Available`| rejects (filtered) |
640
+
|`restricted`| the forced preferred width `w`| preferred widths disagree, or no width-`w` mask fits | rejects (filtered) |
641
+
|`best-effort`| greedy narrowest zone mask that fits by `Available` (width = span) | even all N zones can't cover the request (pod runs unaligned) | passes (best-effort never rejects) |
642
+
643
+
For the two rejecting policies, `aligned` is the *same bit the predicate computes*, so a sunk node
644
+
is one the predicate would filter — the score just reorders the funnel. For `best-effort`,
645
+
`aligned=false` is the unaligned case: still selectable (worst-but-finite score), because
646
+
`best-effort` offers no other node any guarantee.
629
647
630
648
### Notes
631
649
632
650
- Scoring runs on the same predicted per-zone state as v1, so the prediction caveats carry over —
633
651
but a score is only a *preference*, so a misprediction costs ranking quality, never correctness.
634
-
-`best-effort` scoring is the one place the plugin touches `best-effort` nodes at all; v1 leaves
635
-
them untouched, and the admit decision for `single-numa-node` / `restricted` is unchanged.
652
+
- The span metric is pure zone-count and policy-agnostic, so span-1 scores identically across
653
+
`single-numa-node`, `restricted`, and `best-effort` — a mixed-policy candidate set ranks coherently.
654
+
-**No NUMA-distance awareness in v2.** The score is zone *count* only; inter-zone distance
655
+
(`Zone.Costs`) is not ingested and no scoring seam is reserved for it. A distance metric (e.g. a
656
+
v3 "minimax") would be a separate axis added later if pursued.
0 commit comments