feat: redefine generation_ms as net generation time with stall attribution

CallStats gains stall_ms/stall_count (None = no stall observation);
winner sample triple (wall, stall_ms, stall_count) converted once in
_attempt success branch; net = max(0, wall - stall) converged into the
shared _record_winner_sample used by both the single-path guard and
hedge winner registration. Two-fail hedge rounds record nothing
(no winner to attribute). Design section 4.2; issues #27.
This commit is contained in:
2026-09-14 00:56:54 -04:00
parent 901a193514
commit 53bcf43172
5 changed files with 173 additions and 20 deletions
+47 -1
View File
@@ -107,12 +107,15 @@ class _GenClockTransport:
generation_ms 的口径是"只计 transport 调用本身",故推进必须发生在被包
transport 内部;退避耗时由用例自带的 sleep 闭包推进,与本包装无关。
`stalls`(1.3.8 计划 §3.3): 逐次自控上报的 (stall_ms, stall_count),
fake 自己报数、不经真实计时;None(默认)= 该次调用无沉默观测。
"""
def __init__(self, script, advances, clock):
def __init__(self, script, advances, clock, stalls=None):
self._inner = FakeTransport(script)
self._advances = list(advances)
self._clock = clock
self._stalls = list(stalls) if stalls is not None else None
@property
def calls(self):
@@ -131,6 +134,7 @@ class _GenClockTransport:
content_event,
):
advance = self._advances.pop(0)
stall = self._stalls.pop(0) if self._stalls is not None else None
result = await self._inner.complete(
messages=messages,
source=source,
@@ -141,6 +145,8 @@ class _GenClockTransport:
first_token_event=first_token_event,
content_event=content_event,
)
if stall is not None:
result = dataclasses.replace(result, stall_ms=stall[0], stall_count=stall[1])
self._clock.advance(advance)
return result
@@ -1017,6 +1023,46 @@ class TestGenerationMs:
stats = ctx.snapshot()
assert stats.hedges == 0 and stats.hedge_won is False
async def test_generation_ms_net_of_stall(self):
"""净口径(1.3.8 设计 §4.2): generation_ms = 墙钟 − Σ(超阈值中段沉默)。
FakeClock 推进 5.0s 墙钟、fake 自报 stall 3200ms/2 段 → 净 1800ms;
归因两值进快照,墙钟还原公式 generation_ms + stall_ms == 5000 成立。
推进量取二进制可精确表示值(同类既有用例惯例,防 int 截断抖动)。
"""
clock = FakeClock()
transport = _GenClockTransport([_ok()], [5.0], clock, stalls=[(3200.0, 2)])
mw, *_ = _harness([_src("a")], [], clock=clock, transport=transport)
ctx = self._ctx(clock)
resp = await mw(dataclasses.replace(_REQ, call_context=ctx))
assert resp.content == "ok"
stats = ctx.snapshot()
assert stats.generation_ms == 1800
assert stats.stall_ms == 3200 and stats.stall_count == 2
assert stats.generation_ms + stats.stall_ms == 5000 # 墙钟还原公式
async def test_generation_ms_clamps_negative_to_zero(self):
"""沉默上报超过墙钟(异常数据/钟偏差防御) → 净口径 clamp 到 0(N4),归因原样保留。"""
clock = FakeClock()
transport = _GenClockTransport([_ok()], [5.0], clock, stalls=[(6000.0, 3)])
mw, *_ = _harness([_src("a")], [], clock=clock, transport=transport)
ctx = self._ctx(clock)
await mw(dataclasses.replace(_REQ, call_context=ctx))
stats = ctx.snapshot()
assert stats.generation_ms == 0
assert stats.stall_ms == 6000 and stats.stall_count == 3
async def test_non_stream_stall_none_no_deduction(self):
"""stall None(非流式/增量 chunk<2,排除条款) → 净口径不退减(=墙钟),归因 None。"""
clock = FakeClock()
transport = _GenClockTransport([_ok()], [5.0], clock) # 不报 stall = 无观测
mw, *_ = _harness([_src("a")], [], clock=clock, transport=transport)
ctx = self._ctx(clock)
await mw(dataclasses.replace(_REQ, call_context=ctx))
stats = ctx.snapshot()
assert stats.generation_ms == 5000
assert stats.stall_ms is None and stats.stall_count is None
class TestLogicalAttemptCounting:
"""尝试登记在 transport 调用**之前**(1.3.5 设计 §4)。