evalstone/evalscope/tests/perf/test_workload_trace_e2e.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

294 lines
10 KiB
Python

"""End-to-end tests for workload_trace: real HTTP server + full dispatch pipeline."""
import asyncio
import json
import os
import time
from aiohttp import web
from evalscope.perf.arguments import Arguments
from evalscope.perf.core.http_client import AioHttpClient
from evalscope.perf.core.strategies.open_loop import OpenLoopStrategy
from evalscope.perf.plugin.api.openai_api import OpenaiPlugin
from evalscope.perf.plugin.datasets.workload_trace import WorkloadTraceDatasetPlugin
from evalscope.perf.utils.benchmark_util import BenchmarkData
from evalscope.perf.utils.body_meta import BODY_META_PREFIX
class EchoServer:
"""In-process aiohttp server that records arrival times and request bodies."""
def __init__(self):
self.arrivals: list[float] = []
self.bodies: list[dict] = []
self.headers_log: list[dict] = []
self._runner = None
self.port = None
async def _handle(self, request: web.Request) -> web.Response:
self.arrivals.append(time.perf_counter())
body = await request.json()
self.bodies.append(body)
self.headers_log.append(dict(request.headers))
return web.json_response({
'id': 'chatcmpl-1',
'object': 'chat.completion',
'choices': [{
'index': 0,
'message': {'role': 'assistant', 'content': 'ok'},
'finish_reason': 'stop',
}],
'usage': {'prompt_tokens': 5, 'completion_tokens': 1, 'total_tokens': 6},
})
async def start(self) -> int:
app = web.Application()
app.router.add_post('/v1/chat/completions', self._handle)
self._runner = web.AppRunner(app)
await self._runner.setup()
site = web.TCPSite(self._runner, 'localhost', 0)
await site.start()
self.port = site._server.sockets[0].getsockname()[1]
return self.port
async def stop(self):
if self._runner:
await self._runner.cleanup()
def _make_trace_file(records, tmp_path):
path = os.path.join(tmp_path, 'trace.jsonl')
with open(path, 'w') as f:
for r in records:
f.write(json.dumps(r) + '\n')
return path
def _make_args(trace_path, port, **kwargs):
return Arguments(
url=f'http://localhost:{port}/v1/chat/completions',
dataset='workload_trace',
dataset_path=trace_path,
open_loop=True,
rate=1,
**kwargs,
)
async def _run_e2e(records, tmp_path, dataset_args=None, warmup_num=0):
"""Spin up echo server, run full pipeline, return (server, results)."""
server = EchoServer()
await server.start()
try:
path = _make_trace_file(records, tmp_path)
extra = {}
if dataset_args:
extra['dataset_args'] = dataset_args
if warmup_num:
extra['warmup_num'] = warmup_num
args = _make_args(path, server.port, **extra)
for attr in ('parallel', 'number', 'rate'):
v = getattr(args, attr)
if isinstance(v, list):
setattr(args, attr, v[0])
api_plugin = OpenaiPlugin(args)
dataset_plugin = WorkloadTraceDatasetPlugin(args)
requests = []
for messages in dataset_plugin.build_messages():
request = api_plugin.build_request(messages)
requests.append(request)
async def request_gen():
for r in requests:
yield r, False
queue = asyncio.Queue()
results: list[BenchmarkData] = []
client = AioHttpClient(args, api_plugin)
try:
strategy = OpenLoopStrategy(args, api_plugin, client, queue, request_gen())
await strategy.run()
while not queue.empty():
results.append(await queue.get())
finally:
await client.__aexit__(None, None, None)
return server, results
except Exception:
await server.stop()
raise
def _run(coro):
return asyncio.run(coro)
# ---------------------------------------------------------------------------
# E2E Tests
# ---------------------------------------------------------------------------
class TestE2EArrivalTiming:
def test_arrival_intervals_match_trace(self, tmp_path):
"""Requests arrive at intervals matching the trace timestamps (±80ms)."""
records = [
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'a'}]}, 'timestamp': 100.0},
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'b'}]}, 'timestamp': 100.3},
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'c'}]}, 'timestamp': 100.8},
]
async def go():
server, _ = await _run_e2e(records, tmp_path)
await server.stop()
return server
server = _run(go())
assert len(server.arrivals) == 3
intervals = [server.arrivals[i + 1] - server.arrivals[i] for i in range(2)]
assert abs(intervals[0] - 0.3) < 0.08, f'interval[0]={intervals[0]:.3f}, expected ~0.3'
assert abs(intervals[1] - 0.5) < 0.08, f'interval[1]={intervals[1]:.3f}, expected ~0.5'
def test_speed_scaling_compresses_intervals(self, tmp_path):
"""speed=2.0 halves the inter-arrival gaps."""
records = [
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'a'}]}, 'timestamp': 0.0},
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'b'}]}, 'timestamp': 0.6},
]
async def go():
server, _ = await _run_e2e(records, tmp_path, dataset_args={'speed': 2.0})
await server.stop()
return server
server = _run(go())
assert len(server.arrivals) == 2
interval = server.arrivals[1] - server.arrivals[0]
assert abs(interval - 0.3) < 0.08, f'interval={interval:.3f}, expected ~0.3'
class TestE2EBodyPassthrough:
def test_body_arrives_verbatim(self, tmp_path):
"""All body fields reach the server unchanged; no body meta keys leak."""
body = {
'model': 'qwen-72b',
'messages': [{'role': 'user', 'content': 'hello'}],
'temperature': 0.9,
'max_tokens': 100,
'top_p': 0.95,
'seed': 42,
}
records = [{'body': body, 'timestamp': 0.0}]
async def go():
server, _ = await _run_e2e(records, tmp_path)
await server.stop()
return server
server = _run(go())
received = server.bodies[0]
assert received['temperature'] == 0.9
assert received['max_tokens'] == 100
assert received['top_p'] == 0.95
assert received['seed'] == 42
assert received['model'] == 'qwen-72b'
for key in received:
assert not key.startswith(BODY_META_PREFIX), f'body meta key leaked: {key}'
def test_body_meta_headers_applied(self, tmp_path):
"""Per-request headers from trace are merged into HTTP request headers."""
records = [{
'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'hi'}]},
'timestamp': 0.0,
'headers': {'X-Canary': 'true'},
}]
async def go():
server, _ = await _run_e2e(records, tmp_path)
await server.stop()
return server
server = _run(go())
assert server.headers_log[0].get('X-Canary') == 'true'
def test_hop_by_hop_headers_stripped(self, tmp_path):
"""Hop-by-hop headers from trace data are stripped before sending."""
records = [{
'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'hi'}]},
'timestamp': 0.0,
'headers': {'X-Custom': 'keep', 'Host': 'evil.com', 'Connection': 'close'},
}]
async def go():
server, _ = await _run_e2e(records, tmp_path)
await server.stop()
return server
server = _run(go())
sent_headers = server.headers_log[0]
assert sent_headers.get('X-Custom') == 'keep'
# Host should be set by aiohttp to the actual target, not the trace value
assert sent_headers.get('Host') != 'evil.com'
def test_request_id_propagated(self, tmp_path):
"""request_id flows through to BenchmarkData.request_id (not trace_id)."""
records = [{
'body': {'model': 'm', 'messages': [{'role': 'user', 'content': 'hi'}]},
'timestamp': 0.0,
'request_id': 'req-abc-123',
}]
async def go():
server, results = await _run_e2e(records, tmp_path)
await server.stop()
return results
results = _run(go())
assert len(results) == 1
assert results[0].request_id == 'req-abc-123'
assert results[0].trace_id is None
class TestE2EWarmup:
def test_warmup_excluded_from_results(self, tmp_path):
"""Warmup requests have is_warmup=True; non-warmup have is_warmup=False."""
records = [
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': str(i)}]}, 'timestamp': float(i) * 0.1}
for i in range(5)
]
async def go():
server, results = await _run_e2e(records, tmp_path, warmup_num=2)
await server.stop()
return results
results = _run(go())
assert len(results) == 5
warmup_flags = [r.is_warmup for r in results]
assert sum(warmup_flags) == 2
def test_warmup_continuous_timing(self, tmp_path):
"""Warmup and benchmark requests share a single continuous arrival schedule."""
records = [
{'body': {'model': 'm', 'messages': [{'role': 'user', 'content': str(i)}]}, 'timestamp': float(i) * 0.2}
for i in range(4)
]
async def go():
server, _ = await _run_e2e(records, tmp_path, warmup_num=2)
await server.stop()
return server
server = _run(go())
assert len(server.arrivals) == 4
intervals = [server.arrivals[i + 1] - server.arrivals[i] for i in range(3)]
for idx, interval in enumerate(intervals):
assert abs(interval - 0.2) < 0.08, f'interval[{idx}]={interval:.3f}, expected ~0.2'