P2: all routing policies read real state via eff_ accessors + ablation harness
InstanceState.eff_{num_requests,pending_prefill,ongoing_decode,ongoing_tokens}
= max(shadow, real) when feed fresh (fixes 30s-stale under-count, keeps
in-flight RaceFix), plus real-only r_max_prefill_remaining / r_kv_used_frac.
Wired into load_only, lmetric, sticky, unified(_kv_both), unified_v3, and
snapshot logging. Feed off => identical to before. run_v3_trace.sh gains ES=1
toggle (always deploys enhanced proxy); run_ablation_es.sh runs each config
ES0-vs-ES1 to test whether real state changes policy performance/ranking.
All unit-tested without GPU.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,50 @@ class InstanceState:
|
||||
# when the feed is disabled/stale. Set by _engine_state_poll_loop.
|
||||
self.real_state: dict | None = None
|
||||
|
||||
# ---- effective-load accessors (P2): prefer REAL engine state when the
|
||||
# feed is fresh, else the proxy shadow counter. We take max(shadow, real)
|
||||
# for load so we never under-count: REAL fixes the 30s-stale under-count,
|
||||
# while the shadow's atomic pre-await reservation still covers the
|
||||
# in-flight window (preserving the RaceFix against concurrent picks).
|
||||
def eff_num_requests(self) -> float:
|
||||
rs = self.real_state
|
||||
if rs is not None:
|
||||
return max(self.num_requests,
|
||||
rs.get("num_running", 0) + rs.get("num_waiting", 0))
|
||||
return self.num_requests
|
||||
|
||||
def eff_pending_prefill(self) -> float:
|
||||
rs = self.real_state
|
||||
if rs is not None:
|
||||
return max(self.pending_prefill_tokens,
|
||||
rs.get("pending_prefill_tokens", 0))
|
||||
return self.pending_prefill_tokens
|
||||
|
||||
def eff_ongoing_decode(self) -> float:
|
||||
rs = self.real_state
|
||||
if rs is not None:
|
||||
return max(self.ongoing_decode_tokens,
|
||||
rs.get("ongoing_decode_tokens", 0))
|
||||
return self.ongoing_decode_tokens
|
||||
|
||||
def eff_ongoing_tokens(self) -> float:
|
||||
rs = self.real_state
|
||||
if rs is not None:
|
||||
return max(self.ongoing_tokens,
|
||||
rs.get("pending_prefill_tokens", 0)
|
||||
+ rs.get("ongoing_decode_tokens", 0))
|
||||
return self.ongoing_tokens
|
||||
|
||||
def r_max_prefill_remaining(self) -> int:
|
||||
rs = self.real_state
|
||||
return int(rs.get("max_prefill_remaining", 0)) if rs is not None else 0
|
||||
|
||||
def r_kv_used_frac(self) -> float:
|
||||
rs = self.real_state
|
||||
if rs is not None:
|
||||
return float(rs.get("gpu_kv_used_frac", 0.0) or 0.0)
|
||||
return 0.0
|
||||
|
||||
def estimate_cache_hit(self, token_ids: list[int] | None) -> int:
|
||||
if not token_ids or len(token_ids) < BLOCK_SIZE:
|
||||
return 0
|
||||
@@ -277,11 +321,19 @@ def snapshot_workers(
|
||||
"cached_blocks": len(inst.cached_blocks),
|
||||
"cache_hit": cache_hit,
|
||||
"new_prefill": new_prefill,
|
||||
"score_linear": (inst.ongoing_tokens
|
||||
# scores reflect the ACTUAL decision basis (eff = real-or-shadow).
|
||||
"score_linear": (inst.eff_ongoing_tokens()
|
||||
+ _p_offload_penalty(inst)
|
||||
- CACHE_HIT_ALPHA * cache_hit),
|
||||
"score_lmetric": (inst.pending_prefill_tokens + new_prefill)
|
||||
* inst.num_requests,
|
||||
"score_lmetric": (inst.eff_pending_prefill() + new_prefill)
|
||||
* inst.eff_num_requests(),
|
||||
# P2: real-state fields when the feed is fresh (None otherwise).
|
||||
"real_num_requests": (inst.eff_num_requests()
|
||||
if inst.real_state is not None else None),
|
||||
"real_max_prefill_remaining": (inst.r_max_prefill_remaining()
|
||||
if inst.real_state is not None else None),
|
||||
"real_kv_used_frac": (inst.r_kv_used_frac()
|
||||
if inst.real_state is not None else None),
|
||||
})
|
||||
return snap
|
||||
|
||||
@@ -297,20 +349,20 @@ def pick_instance(instances: list[InstanceState], token_ids: list[int] | None,
|
||||
Instances doing P-role offloads get a large penalty to steer
|
||||
WARM/MEDIUM traffic away.
|
||||
"""
|
||||
avg_load = max(sum(i.ongoing_tokens for i in instances) / len(instances), 1.0)
|
||||
avg_load = max(sum(i.eff_ongoing_tokens() for i in instances) / len(instances), 1.0)
|
||||
|
||||
if session_id and session_id in affinity:
|
||||
idx = affinity[session_id]
|
||||
if idx < len(instances):
|
||||
inst = instances[idx]
|
||||
if (inst.ongoing_tokens <= avg_load * SETTINGS.overload_factor
|
||||
if (inst.eff_ongoing_tokens() <= avg_load * SETTINGS.overload_factor
|
||||
and inst.active_p_offloads == 0):
|
||||
return inst, idx
|
||||
|
||||
best_idx, best_score = 0, float("inf")
|
||||
for i, inst in enumerate(instances):
|
||||
cache_hit = inst.estimate_cache_hit(token_ids)
|
||||
score = (inst.ongoing_tokens + _p_offload_penalty(inst)
|
||||
score = (inst.eff_ongoing_tokens() + _p_offload_penalty(inst)
|
||||
- CACHE_HIT_ALPHA * cache_hit)
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
@@ -334,7 +386,7 @@ def pick_instance_load_only(
|
||||
isolate the locality contribution of cache-aware policies.
|
||||
"""
|
||||
best_idx = min(range(len(instances)),
|
||||
key=lambda i: instances[i].num_requests)
|
||||
key=lambda i: instances[i].eff_num_requests())
|
||||
return instances[best_idx], best_idx
|
||||
|
||||
|
||||
@@ -357,7 +409,7 @@ def pick_instance_sticky(
|
||||
if idx < len(instances):
|
||||
return instances[idx], idx
|
||||
best_idx = min(range(len(instances)),
|
||||
key=lambda i: instances[i].num_requests)
|
||||
key=lambda i: instances[i].eff_num_requests())
|
||||
if session_id:
|
||||
affinity[session_id] = best_idx
|
||||
return instances[best_idx], best_idx
|
||||
@@ -378,8 +430,8 @@ def pick_instance_lmetric(instances: list[InstanceState], token_ids: list[int] |
|
||||
for i, inst in enumerate(instances):
|
||||
cache_hit = inst.estimate_cache_hit(token_ids)
|
||||
new_prefill = max(0, input_length - cache_hit)
|
||||
p_tokens = inst.pending_prefill_tokens + new_prefill
|
||||
bs = inst.num_requests
|
||||
p_tokens = inst.eff_pending_prefill() + new_prefill
|
||||
bs = inst.eff_num_requests()
|
||||
score = p_tokens * bs
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
@@ -416,7 +468,7 @@ def pick_instance_unified_hybrid(
|
||||
"""
|
||||
global _unified_fallback_rr_counter
|
||||
n = len(instances)
|
||||
avg_reqs = max(sum(i.num_requests for i in instances) / n, 1.0)
|
||||
avg_reqs = max(sum(i.eff_num_requests() for i in instances) / n, 1.0)
|
||||
|
||||
decision: dict = {
|
||||
"decision": "lmetric_fallback",
|
||||
@@ -439,9 +491,9 @@ def pick_instance_unified_hybrid(
|
||||
decision["affinity_idx"] = a_idx
|
||||
decision["affinity_cache_hit"] = a_hit
|
||||
decision["affinity_cache_ratio"] = a_ratio
|
||||
decision["affinity_num_requests"] = a_inst.num_requests
|
||||
decision["affinity_num_requests"] = a_inst.eff_num_requests()
|
||||
if (a_ratio > 0.5
|
||||
and a_inst.num_requests <= avg_reqs * SETTINGS.overload_factor):
|
||||
and a_inst.eff_num_requests() <= avg_reqs * SETTINGS.overload_factor):
|
||||
decision["decision"] = "affinity"
|
||||
decision["chosen_idx"] = a_idx
|
||||
return a_inst, a_idx, decision
|
||||
@@ -460,9 +512,10 @@ def pick_instance_unified_hybrid(
|
||||
for i, inst in enumerate(instances):
|
||||
cache_hit = inst.estimate_cache_hit(token_ids)
|
||||
new_prefill = max(0, input_length - cache_hit)
|
||||
p_tokens = inst.pending_prefill_tokens + new_prefill
|
||||
decode_pen = SETTINGS.lmetric_decode_weight * inst.ongoing_decode_tokens
|
||||
bs = inst.num_requests
|
||||
# P2: effective (real-or-shadow) load signals.
|
||||
p_tokens = inst.eff_pending_prefill() + new_prefill
|
||||
decode_pen = SETTINGS.lmetric_decode_weight * inst.eff_ongoing_decode()
|
||||
bs = inst.eff_num_requests()
|
||||
score = (p_tokens + decode_pen) * max(bs, 1)
|
||||
keys.append((score, new_prefill, bs, i))
|
||||
|
||||
@@ -669,10 +722,10 @@ def pick_instance_unified_v3(
|
||||
# Gate 2: affinity host must be busy with concurrent decodes — that's
|
||||
# what migrating decode-traffic-away buys us. If the host is idle
|
||||
# there's no point.
|
||||
if prefill_host.ongoing_decode_tokens < SETTINGS.v3_min_prefill_decode_busy:
|
||||
if prefill_host.eff_ongoing_decode() < SETTINGS.v3_min_prefill_decode_busy:
|
||||
decision["v3_reason"] = (
|
||||
f"prefill_host_not_busy "
|
||||
f"(ongoing_decode_tokens={prefill_host.ongoing_decode_tokens} < "
|
||||
f"(ongoing_decode_tokens={prefill_host.eff_ongoing_decode()} < "
|
||||
f"{SETTINGS.v3_min_prefill_decode_busy})"
|
||||
)
|
||||
return prefill_host, prefill_idx, decision, None
|
||||
@@ -683,12 +736,8 @@ def pick_instance_unified_v3(
|
||||
cutoff = now_mono - SETTINGS.v3_recent_mig_window_s
|
||||
|
||||
def _real_load(inst):
|
||||
# P2: prefer REAL engine state (running+waiting) over the proxy's
|
||||
# 30s-stale shadow num_requests, when the engine-state feed is fresh.
|
||||
rs = getattr(inst, "real_state", None)
|
||||
if rs is not None:
|
||||
return rs.get("num_running", 0) + rs.get("num_waiting", 0)
|
||||
return inst.num_requests
|
||||
# P2: effective (real-or-shadow) request load; see eff_num_requests.
|
||||
return inst.eff_num_requests()
|
||||
|
||||
def effective_load(inst):
|
||||
# Drop expired entries lazily.
|
||||
|
||||
Reference in New Issue
Block a user