Skip to content

Commit c77b193

Browse files
Scale semantic-score keepScore terms onto a shared [0,1] range.
Normalize deadline urgency, fairness, and preempt cost so default weights stay meaningful under extreme inputs. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d79716a commit c77b193

3 files changed

Lines changed: 110 additions & 41 deletions

File tree

docs/architecture/semantic-score.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -134,37 +134,40 @@ keepScore(s) =
134134
+ w_C * normalize(preemptCost(s))
135135
```
136136

137-
| Term | Definition | If missing |
138-
|------|------------|------------|
137+
| Term | Definition (all ≈ `[0,1]`) | If missing |
138+
|------|----------------------------|------------|
139139
| `phaseProtect` | `tool_loop`/`lock` → 1.0; `llm_wait` → 0.2; `idle` → 0.0 | 0.5 |
140-
| `urgency` | See §5.4 | 0 |
141-
| `fairness` | `waitSec / (1 + attainedServiceSec)` | 0 |
142-
| `preemptCost` | Snapshot cost/size or `KeepAliveH` | 1 |
140+
| `urgency` | See §5.4 (online + prior, both scaled to `[0,1]`) | 0 |
141+
| `fairness` | Soft map of `r=wait/(1+attained)`: `1 − 1/(1+r)``[0,1)` | 0 |
142+
| `preemptCost` | `clamp(log1p(H) / log1p(H_ref), 0, 1)` with `H_ref=1e6` | H=1 → small |
143143

144-
Default weights: `w_L=3`, `w_U=2`, `w_F=2`, `w_C=1` (env-tunable).
144+
Default weights: `w_L=3`, `w_U=2`, `w_F=2`, `w_C=1` (env-tunable).
145+
Terms share a common scale so weights stay meaningful (no raw `1/sec` or unbounded wait).
145146

146147
Low score ⇒ yieldable phase, low urgency, already well-served, cheap to restore → kick first.
147148

148149
### 5.4 `urgency` (online + optional L3 prior)
149150

150151
```text
151-
urgency_online:
152+
urgency_online: # ∈ [0,1]
152153
if deadline:
153-
1 / max(ε, seconds_until(deadline)) # deadline pressure (no step count)
154+
1 / (1 + max(0, seconds_until(deadline)))
154155
else if API priority:
155-
map(priority)
156+
map(priority) into [0,1]
156157
else:
157158
0
158159
159-
urgency_prior: # from taskProfile — §6 (vLLM-SR traits, not expectedSteps)
160-
clamp(0.5 + complexitySignal, 0, 1) # continuous SR hard−easy signal
161-
+ α * embeddingSim # optional affinity boost
160+
urgency_prior: # ∈ [0,1] — §6 (vLLM-SR traits, not expectedSteps)
161+
clamp(
162+
clamp(0.5 + complexitySignal, 0, 1) # continuous SR hard−easy signal
163+
+ α * clamp(embeddingSim, 0, 1),
164+
0, 1)
162165
# domain: affinity / pool filter when Worker pools exist; else unused in score
163166
# difficultyTier (if present) is debug-only; not used in keepScore
164167
165168
urgency =
166169
if taskProfile.confidence high:
167-
mix(urgency_online, urgency_prior) # online dominates when present
170+
mix(urgency_online, urgency_prior) # both already on [0,1]
168171
else:
169172
urgency_online
170173
```
@@ -306,7 +309,7 @@ demos/.../driver ──classify(taskText)──► taskProfile
306309
| Path | Change |
307310
|------|--------|
308311
| `internal/signals/types.go` | `TaskProfile`: `ComplexitySignal`, `Domain`, `EmbeddingSim`, `Confidence`, `ModelID`, `ScoredAt`; `DifficultyTier` debug-only; stop using `ExpectedSteps*` in policy |
309-
| `internal/policy/semantic_score.go` | `urgencyPrior` = `clamp(0.5+complexitySignal) + α·embeddingSim`; ignore prior if `confidence < 0.3`; `urgency_online` from `deadline` only |
312+
| `internal/policy/semantic_score.go` | `urgencyPrior` / online / fairness / preempt all scaled to ≈`[0,1]`; ignore prior if `confidence < 0.3` |
310313
| `internal/policy/semantic_score_test.go` | Cases: higher `complexitySignal` outranks lower among unlocked peers; low confidence ignores prior |
311314
| CP / Kind env | `SEMANTIC_PRIOR_MIX`, `SEMANTIC_EMBED_ALPHA` (see §8); **no** HF model load in `controlplane` |
312315

internal/policy/semantic_score.go

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,11 @@ func urgency(sem signals.SemanticResource, priorMix, embedAlpha float64, now tim
196196
func urgencyOnline(sem signals.SemanticResource, now time.Time) float64 {
197197
if sem.Deadline != nil && !sem.Deadline.IsZero() {
198198
sec := sem.Deadline.Sub(now).Seconds()
199-
if sec < 1e-3 {
200-
sec = 1e-3
199+
if sec < 0 {
200+
sec = 0
201201
}
202-
return 1.0 / sec
202+
// Maps remaining time → [0,1]: due now ⇒ 1, far ⇒ ~0.
203+
return 1.0 / (1.0 + sec)
203204
}
204205
return 0
205206
}
@@ -209,32 +210,17 @@ func urgencyPrior(sem signals.SemanticResource, embedAlpha float64) float64 {
209210
return 0
210211
}
211212
tp := sem.TaskProfile
212-
// SR continuous signal → [0,1] contribution: clamp(0.5 + signal).
213+
// SR continuous signal → [0,1]: clamp(0.5 + signal).
213214
complexity := 0.0
214215
if tp.ComplexitySignal != nil {
215-
complexity = 0.5 + *tp.ComplexitySignal
216-
if complexity < 0 {
217-
complexity = 0
218-
}
219-
if complexity > 1 {
220-
complexity = 1
221-
}
222-
}
223-
sim := tp.EmbeddingSim
224-
if sim < 0 {
225-
sim = 0
226-
}
227-
if sim > 1 {
228-
sim = 1
216+
complexity = clamp01(0.5 + *tp.ComplexitySignal)
229217
}
218+
sim := clamp01(tp.EmbeddingSim)
230219
if embedAlpha < 0 {
231220
embedAlpha = 0
232221
}
233-
prior := complexity + embedAlpha*sim
234-
if prior <= 0 {
235-
return 0
236-
}
237-
return prior
222+
// Keep prior on the same [0,1] scale as other keepScore terms.
223+
return clamp01(complexity + embedAlpha*sim)
238224
}
239225

240226
func fairness(sem signals.SemanticResource) float64 {
@@ -246,15 +232,33 @@ func fairness(sem signals.SemanticResource) float64 {
246232
if wait < 0 {
247233
wait = 0
248234
}
249-
return wait / (1 + att)
235+
r := wait / (1 + att) // raw ratio, unbounded
236+
// Soft map [0,∞) → [0,1): 0→0, 1→0.5, ∞→1.
237+
return 1.0 - 1.0/(1.0+r)
250238
}
251239

240+
// preemptHRef is the KeepAliveH / snapshot-cost scale that maps to ~1.0 after normalize.
241+
const preemptHRef = 1e6
242+
252243
func normalizePreempt(h float64) float64 {
253-
if h <= 0 || math.IsInf(h, 1) {
244+
if h <= 0 {
245+
return 0
246+
}
247+
if math.IsInf(h, 1) {
248+
return 1
249+
}
250+
// log1p(H) / log1p(H_ref) ∈ (0,1] for H ≤ H_ref; clamp above.
251+
return clamp01(math.Log1p(h) / math.Log1p(preemptHRef))
252+
}
253+
254+
func clamp01(x float64) float64 {
255+
if x < 0 {
256+
return 0
257+
}
258+
if x > 1 {
254259
return 1
255260
}
256-
// Soft compress so large H does not dominate phase/urgency weights.
257-
return math.Log1p(h)
261+
return x
258262
}
259263

260264
func envFloat(k string, def float64) float64 {

internal/policy/semantic_score_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,65 @@ func TestSemanticScorePrefersKickLowWaitHighAttained(t *testing.T) {
177177
t.Fatalf("victim=%q want hog (low fairness)", res.VictimID)
178178
}
179179
}
180+
181+
func TestSemanticScoreKeepsNearDeadlineOverFar(t *testing.T) {
182+
p := policy.NewSemanticScore()
183+
now := time.Now()
184+
p.Now = func() time.Time { return now }
185+
near := now.Add(2 * time.Second)
186+
far := now.Add(2 * time.Hour)
187+
workers := []types.Worker{
188+
{ID: "w1", MaxSlots: 1, UsedSlots: 1, Healthy: true, RegisteredAt: now},
189+
}
190+
running := []types.Sandbox{
191+
{ID: "far", WorkerID: "w1", State: types.SandboxRunning, CreatedAt: now.Add(-time.Hour)},
192+
{ID: "near", WorkerID: "w1", State: types.SandboxRunning, CreatedAt: now},
193+
}
194+
sandboxSig := map[string]signals.SandboxSignals{
195+
"far": {SandboxID: "far", Semantic: signals.SemanticResource{
196+
Phase: signals.PhaseLLMWait, Deadline: &far,
197+
}},
198+
"near": {SandboxID: "near", Semantic: signals.SemanticResource{
199+
Phase: signals.PhaseLLMWait, Deadline: &near,
200+
}},
201+
}
202+
res, err := p.Place(context.Background(), policy.PlaceRequest{
203+
SandboxID: "new", Workers: workers, Running: running, SandboxSignals: sandboxSig,
204+
})
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
if res.VictimID != "far" {
209+
t.Fatalf("victim=%q want far (lower urgency_online)", res.VictimID)
210+
}
211+
}
212+
213+
func TestSemanticScoreHugePreemptCostDoesNotBeatToolLoopFilter(t *testing.T) {
214+
// Unlocked idle with tiny H vs locked tool_loop with huge H: still kick idle.
215+
p := policy.NewSemanticScore()
216+
now := time.Now()
217+
workers := []types.Worker{
218+
{ID: "w1", MaxSlots: 1, UsedSlots: 1, Healthy: true, RegisteredAt: now},
219+
}
220+
running := []types.Sandbox{
221+
{ID: "tool", WorkerID: "w1", State: types.SandboxRunning, CreatedAt: now.Add(-time.Hour)},
222+
{ID: "idle", WorkerID: "w1", State: types.SandboxRunning, CreatedAt: now},
223+
}
224+
sandboxSig := map[string]signals.SandboxSignals{
225+
"tool": {SandboxID: "tool", KeepAliveH: 1e18, Semantic: signals.SemanticResource{
226+
Phase: signals.PhaseToolLoop, Lock: true,
227+
}},
228+
"idle": {SandboxID: "idle", KeepAliveH: 1, Semantic: signals.SemanticResource{
229+
Phase: signals.PhaseIdle,
230+
}},
231+
}
232+
res, err := p.Place(context.Background(), policy.PlaceRequest{
233+
SandboxID: "new", Workers: workers, Running: running, SandboxSignals: sandboxSig,
234+
})
235+
if err != nil {
236+
t.Fatal(err)
237+
}
238+
if res.VictimID != "idle" {
239+
t.Fatalf("victim=%q want idle (lock filter before cost)", res.VictimID)
240+
}
241+
}

0 commit comments

Comments
 (0)