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 <noreply@anthropic.com>
This commit is contained in:
sora 2026-09-15 02:33:21 +00:00
parent 3be48addfc
commit 4eb853c8fc

View File

@ -166,6 +166,11 @@ class AdaptiveGate:
self._loop = None # loop the cond/probe-task are bound to self._loop = None # loop the cond/probe-task are bound to
self._contended = 0 # acquire() waits this probe interval (demand) self._contended = 0 # acquire() waits this probe interval (demand)
self._interval_fails = 0 # failed releases this probe interval 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, self.stats = {'probe': 0, 'ramp': 0, 'hold_queue': 0, 'backoff_fail': 0,
'backoff_queue': 0, 'ramp_demand': 0} 'backoff_queue': 0, 'ramp_demand': 0}
@ -209,7 +214,9 @@ class AdaptiveGate:
def release(self, ok: bool) -> None: def release(self, ok: bool) -> None:
self._inflight = max(0, self._inflight - 1) 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 self._interval_fails += 1
before = self.limit before = self.limit
self.limit = max(self.LO, self.limit * 0.7) 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 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 acquire) and the interval was failure-free. Failures still cut x0.7
per release, so a drowning backend shrinks the gate immediately.""" per release, so a drowning backend shrinks the gate immediately."""
if self._interval_fails == 0 and self._contended > 0 \ if self._interval_fails == 0 and self._interval_ok > 0 \
and int(self.limit) < self.HI: and self._contended > 0 and int(self.limit) < self.HI:
self.limit = min(self.HI, self.limit + 1) self.limit = min(self.HI, self.limit + 1)
self.stats['ramp_demand'] += 1 self.stats['ramp_demand'] += 1
self._push_limit() self._push_limit()
self._interval_fails = 0 self._interval_fails = 0
self._interval_ok = 0
self._contended = 0 self._contended = 0
self._wake() self._wake()
@ -278,8 +286,10 @@ class AdaptiveGate:
self.limit = max(self.LO, self.limit - 1) self.limit = max(self.LO, self.limit - 1)
self.stats['backoff_queue'] += 1 self.stats['backoff_queue'] += 1
self._push_limit() 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 # 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.limit = min(self.HI, self.limit + 1)
self.stats['ramp'] += 1 self.stats['ramp'] += 1
self._push_limit() self._push_limit()