From 4eb853c8fc5a9467a7d6950c699f46c9721ac737 Mon Sep 17 00:00:00 2001 From: sora <2075279110@qq.com> Date: Tue, 15 Sep 2026 02:33:21 +0000 Subject: [PATCH] Gate ramp requires completions: zero throughput = hold, not +1 Ramping on demand alone was dangerous with slow endpoints: if the 2 in-flight longbench_v2 requests hang, 'no failures + waiters queued' kept adding +1 every 5s all the way to 96 -- piling prefills onto a server that had not answered anything. Both ramp paths (demand-driven and /metrics) now require at least one SUCCESSFUL completion in the probe interval; hangs hold the gate until read-timeouts fire and the x0.7 backoff takes over. Unit-verified: hang 4 probe intervals with 20 waiters -> limit stays 2; one success -> +1; one failure -> x0.7. Co-Authored-By: Claude --- evalharness/model/pool.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/evalharness/model/pool.py b/evalharness/model/pool.py index 6ce27cb..9b95c2a 100644 --- a/evalharness/model/pool.py +++ b/evalharness/model/pool.py @@ -166,6 +166,11 @@ class AdaptiveGate: self._loop = None # loop the cond/probe-task are bound to self._contended = 0 # acquire() waits this probe interval (demand) self._interval_fails = 0 # failed releases this probe interval + self._interval_ok = 0 # SUCCESSFUL releases this probe interval: + # zero completions = zero throughput, and a + # gate that ramps on demand alone would pile + # 96 concurrent prefills onto a server whose + # first 2 requests have not even answered self.stats = {'probe': 0, 'ramp': 0, 'hold_queue': 0, 'backoff_fail': 0, 'backoff_queue': 0, 'ramp_demand': 0} @@ -209,7 +214,9 @@ class AdaptiveGate: def release(self, ok: bool) -> None: self._inflight = max(0, self._inflight - 1) - if not ok: # multiplicative decrease -- survival first + if ok: + self._interval_ok += 1 + else: # multiplicative decrease -- survival first self._interval_fails += 1 before = self.limit self.limit = max(self.LO, self.limit * 0.7) @@ -238,12 +245,13 @@ class AdaptiveGate: ramp while the cap is the binding constraint (callers had to WAIT on acquire) and the interval was failure-free. Failures still cut x0.7 per release, so a drowning backend shrinks the gate immediately.""" - if self._interval_fails == 0 and self._contended > 0 \ - and int(self.limit) < self.HI: + if self._interval_fails == 0 and self._interval_ok > 0 \ + and self._contended > 0 and int(self.limit) < self.HI: self.limit = min(self.HI, self.limit + 1) self.stats['ramp_demand'] += 1 self._push_limit() self._interval_fails = 0 + self._interval_ok = 0 self._contended = 0 self._wake() @@ -278,8 +286,10 @@ class AdaptiveGate: self.limit = max(self.LO, self.limit - 1) self.stats['backoff_queue'] += 1 self._push_limit() - elif (queue or 0) == 0 and (running is None or running < max(2, int(self.limit))): + elif (queue or 0) == 0 and (running is None or running < max(2, int(self.limit))) \ + and self._interval_ok > 0: # underfed: no queue and running below our own cap -> ramp up + # (still requires completions this interval: no throughput, no ramp) self.limit = min(self.HI, self.limit + 1) self.stats['ramp'] += 1 self._push_limit()