evalstone/evalscope/tests/perf/test_multi_turn_warmup_handoff.py
sora 13274243a0 Bump vendored EvalScope and add K3-ready DPV4 configs.
Keep K3 suite selection and report-schema scoring in bash, merge K3/vision dataset_args into dpv4 yamls, and pin EvalScope at 735d920ee911 with local patches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 07:30:48 +00:00

224 lines
7.7 KiB
Python

"""Multi-turn warmup hand-off tests.
Warmup conversations must occupy the server until measured conversations take
over. A phase barrier would drain occupancy to zero and release the opening
measured cohort against an idle server, which is the defect tracked by #1648.
"""
import asyncio
import json
import os
import sqlite3
import time
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
import pytest
from evalscope.perf.arguments import Arguments
from evalscope.perf.core.pipeline import run_benchmark_pipeline
from evalscope.perf.core.strategies import multi_turn as multi_turn_module
from evalscope.perf.core.strategies.multi_turn import MultiTurnStrategy
from evalscope.perf.plugin.datasets.base import Conversation, Turn
from evalscope.perf.utils.benchmark_util import BenchmarkData
def _make_args(**kwargs: Any) -> Arguments:
number = kwargs.pop('number', 6)
parallel = kwargs.pop('parallel', 3)
warmup_num = kwargs.pop('warmup_num', 0)
args = Arguments(
model='test-model',
api='openai',
number=number,
parallel=parallel,
rate=-1,
warmup_num=warmup_num,
multi_turn=True,
**kwargs,
)
args.number = number
args.parallel = parallel
args.rate = -1
return args
def _conversations(count: int) -> List[Conversation]:
return [[Turn(messages=[{'role': 'user', 'content': f'conv-{i}'}])] for i in range(count)]
class _ApiPlugin:
def build_request(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
return {'messages': messages, 'stream': True}
def parse_responses(self, response_messages: List[Any], request: Optional[str] = None) -> tuple[int, int]:
return 1, 1
class _RecordingClient:
"""Fake server that records occupancy when each request starts."""
def __init__(self) -> None:
self.in_flight = 0
self.events: List[Dict[str, Any]] = []
self._seq = 0
async def post(self, request: Dict[str, Any]) -> BenchmarkData:
seq = self._seq
self._seq += 1
conv_id = request['messages'][0]['content']
start_time = time.perf_counter()
self.events.append({
'conv_id': conv_id,
'occupancy_at_start': self.in_flight,
'order': seq,
})
self.in_flight += 1
try:
for _ in range(2 * (seq + 1)):
await asyncio.sleep(0)
finally:
self.in_flight -= 1
completed_time = time.perf_counter()
return BenchmarkData(
request=json.dumps(request),
start_time=start_time,
completed_time=completed_time,
query_latency=completed_time - start_time,
first_chunk_latency=0.001,
success=True,
is_stream=True,
prompt_tokens=1,
completion_tokens=1,
generated_text='ok',
)
def _run_strategy(args: Arguments) -> _RecordingClient:
client = _RecordingClient()
async def main() -> None:
strategy = MultiTurnStrategy(args, _ApiPlugin(), client, asyncio.Queue(), _conversations(args.total_count))
await strategy.run()
asyncio.run(main())
return client
def _conv_index(conv_id: str) -> int:
return int(conv_id.removeprefix('conv-'))
def test_measured_portion_starts_without_draining() -> None:
parallel = 4
args = _make_args(number=6, parallel=parallel, warmup_num=parallel)
client = _run_strategy(args)
measured = [event for event in client.events if _conv_index(event['conv_id']) >= parallel]
assert measured, 'no measured conversation was dispatched'
first_measured = min(measured, key=lambda event: event['order'])
assert first_measured['occupancy_at_start'] == parallel - 1
later_occupancies = [event['occupancy_at_start'] for event in client.events if event['order'] > 0]
assert later_occupancies and all(occupancy > 0 for occupancy in later_occupancies)
def test_partial_warmup_degrades_gracefully() -> None:
parallel = 4
warmup = 2
args = _make_args(number=6, parallel=parallel, warmup_num=warmup)
client = _run_strategy(args)
opening = sorted(client.events, key=lambda event: event['order'])[:parallel]
assert [event['occupancy_at_start'] for event in opening] == list(range(parallel))
assert sum(1 for event in opening if _conv_index(event['conv_id']) >= warmup) == parallel - warmup
def test_zero_warmup_keeps_all_opening_work_measured() -> None:
parallel = 3
args = _make_args(number=5, parallel=parallel, warmup_num=0)
client = _run_strategy(args)
opening = sorted(client.events, key=lambda event: event['order'])[:parallel]
assert [event['occupancy_at_start'] for event in opening] == list(range(parallel))
assert all(_conv_index(event['conv_id']) < args.number for event in opening)
def test_duration_exempts_warmup_and_caps_measured() -> None:
parallel = 2
measured = 20
args = _make_args(number=measured, parallel=parallel, warmup_num=parallel, duration=0.0)
client = _run_strategy(args)
dispatched = {_conv_index(event['conv_id']) for event in client.events}
assert {0, 1}.issubset(dispatched)
measured_dispatched = {idx for idx in dispatched if idx >= parallel}
assert 1 <= len(measured_dispatched) < measured
class _RecordingLogger:
def __init__(self) -> None:
self.warnings: List[str] = []
def warning(self, msg: str) -> None:
self.warnings.append(msg)
def info(self, msg: str) -> None:
pass
def test_warns_when_multi_turn_warmup_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
recorder = _RecordingLogger()
monkeypatch.setattr(multi_turn_module, 'logger', recorder)
strategy = MultiTurnStrategy(_make_args(number=10, parallel=4, warmup_num=0), _ApiPlugin(), None, asyncio.Queue(), [])
strategy._log_warmup_handoff()
assert len(recorder.warnings) == 1
assert 'Multi-turn warmup is disabled' in recorder.warnings[0]
assert '--warmup-num 4' in recorder.warnings[0]
def test_warns_when_multi_turn_warmup_smaller_than_parallel(monkeypatch: pytest.MonkeyPatch) -> None:
recorder = _RecordingLogger()
monkeypatch.setattr(multi_turn_module, 'logger', recorder)
strategy = MultiTurnStrategy(_make_args(number=10, parallel=4, warmup_num=2), _ApiPlugin(), None, asyncio.Queue(), [])
strategy._log_warmup_handoff()
assert len(recorder.warnings) == 1
assert 'covers only 2 of the 4 concurrency slots' in recorder.warnings[0]
assert '2 measured conversation(s)' in recorder.warnings[0]
def test_silent_when_multi_turn_warmup_covers_parallel(monkeypatch: pytest.MonkeyPatch) -> None:
recorder = _RecordingLogger()
monkeypatch.setattr(multi_turn_module, 'logger', recorder)
strategy = MultiTurnStrategy(_make_args(number=10, parallel=4, warmup_num=4), _ApiPlugin(), None, asyncio.Queue(), [])
strategy._log_warmup_handoff()
assert recorder.warnings == []
def test_warmup_reaches_producer_but_stays_out_of_result_db(tmp_path) -> None:
parallel = 3
number = 5
output_dir = tmp_path / 'multi-turn-e2e'
output_dir.mkdir()
args = _make_args(number=number, parallel=parallel, warmup_num=parallel, outputs_dir=str(output_dir))
client = _RecordingClient()
queue: asyncio.Queue = asyncio.Queue()
strategy = MultiTurnStrategy(args, _ApiPlugin(), client, queue, _conversations(args.total_count))
async def main() -> None:
await run_benchmark_pipeline(strategy.run(), queue, args, _ApiPlugin())
asyncio.run(main())
assert len(client.events) == parallel + number
with sqlite3.connect(os.path.join(str(output_dir), 'benchmark_data.db')) as con:
rows = con.execute('SELECT COUNT(*) FROM result').fetchone()[0]
assert rows == number