Skip to content

Commit 1783672

Browse files
Preserve input weight ordering when no weighers are configured (#918)
filterWeigherPipeline.Run always applied math.Tanh to the requester-supplied input weights via normalizeInputWeights. With high Nova-style values (e.g. 50, 55, 60) the tanh output saturates to ~1.0 within machine epsilon for every input — already at tanh(20) the result is indistinguishable from 1.0 in float64. When the pipeline has no weighers configured, those saturated values flowed straight into sortHostsByWeights, whose comparator is not stable under ties, so Go's sort.Slice returned an order driven by randomized map iteration and the requester's original host ordering was lost. The fix gates normalizeInputWeights on len(p.weighers) > 0 inside Run. When weighers are configured the behavior is unchanged: input weights are tanh-normalized so they live on a comparable scale with the (also tanh-bounded) weigher contributions. When no weighers are configured there is nothing on the other side of the scale to combine with, so we clone the raw request weights and keep the original ordering intact. Filters do not contribute weights — they only remove hosts — so they are deliberately not part of the condition. Covered by TestPipeline_Run_NoWeighers_PreservesInputOrdering, which runs the empty pipeline 50 times against weights {host1: 50, host2: 55, host3: 60} and asserts the descending order [host3, host2, host1] holds every iteration; without the fix this test fails intermittently due to map iteration order. Assisted-by: Claude Code:claude-opus-4-5 [Bash] [Read]
1 parent 41096b8 commit 1783672

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

internal/scheduling/lib/filter_weigher_pipeline.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,16 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1.
274274
traceLog.Info("scheduler: starting pipeline", "hosts", hostsIn)
275275

276276
// Normalize the input weights so we can apply step weights meaningfully.
277-
inWeights := p.normalizeInputWeights(request.GetWeights())
277+
// Only do this if there are weighers to combine with: tanh saturates large
278+
// inputs (e.g. Nova's 50/55/60) to ~1.0, which would destroy the original
279+
// ordering. With no weighers configured, the normalized map flows straight
280+
// to the sort, so we must keep the raw values to preserve that ordering.
281+
var inWeights map[string]float64
282+
if len(p.weighers) > 0 {
283+
inWeights = p.normalizeInputWeights(request.GetWeights())
284+
} else {
285+
inWeights = maps.Clone(request.GetWeights())
286+
}
278287
traceLog.Info("scheduler: input weights", "weights", inWeights)
279288

280289
// Run filters first to reduce the number of hosts.

internal/scheduling/lib/filter_weigher_pipeline_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,41 @@ func TestPipeline_Run(t *testing.T) {
8888
}
8989
}
9090

91+
func TestPipeline_Run_NoWeighers_PreservesInputOrdering(t *testing.T) {
92+
// With no weighers configured, the tanh normalization would saturate
93+
// large input weights to ~1.0 and destroy the requester's ordering. The
94+
// pipeline must skip normalization in that case so the original ordering
95+
// flows through to the sort.
96+
pipeline := &filterWeigherPipeline[mockFilterWeigherPipelineRequest]{}
97+
98+
request := mockFilterWeigherPipelineRequest{
99+
Hosts: []string{"host1", "host2", "host3"},
100+
Weights: map[string]float64{
101+
"host1": 50.0,
102+
"host2": 55.0,
103+
"host3": 60.0,
104+
},
105+
}
106+
107+
// Run many times to surface any non-determinism from map iteration order.
108+
expected := []string{"host3", "host2", "host1"}
109+
for i := range 50 {
110+
result, err := pipeline.Run(request)
111+
if err != nil {
112+
t.Fatalf("expected no error, got %v", err)
113+
}
114+
if len(result.OrderedHosts) != len(expected) {
115+
t.Fatalf("expected %d results, got %d", len(expected), len(result.OrderedHosts))
116+
}
117+
for j, host := range expected {
118+
if result.OrderedHosts[j] != host {
119+
t.Fatalf("iter %d: expected host %s at position %d, got %s (order=%v)",
120+
i, host, j, result.OrderedHosts[j], result.OrderedHosts)
121+
}
122+
}
123+
}
124+
}
125+
91126
func TestPipeline_NormalizeNovaWeights(t *testing.T) {
92127
p := &filterWeigherPipeline[mockFilterWeigherPipelineRequest]{}
93128

0 commit comments

Comments
 (0)