178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
# Copyright (c) Alibaba, Inc. and its affiliates.
|
|
"""
|
|
End-to-end concurrency test for ModelAPI.generate_async().
|
|
|
|
This test verifies that generate_async() truly enables concurrent execution
|
|
by comparing serial vs parallel invocation times against a real LLM endpoint.
|
|
|
|
Requirements:
|
|
- DASHSCOPE_API_KEY (or EVALSCOPE_API_KEY) environment variable must be set
|
|
- Network access to DashScope API
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import time
|
|
import unittest
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv('.env')
|
|
|
|
from evalscope.api.messages import ChatMessageUser
|
|
from evalscope.api.model import GenerateConfig, ModelOutput
|
|
from evalscope.models.openai_compatible import OpenAICompatibleAPI
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
MODEL_NAME = 'qwen-plus'
|
|
BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
|
API_KEY = os.environ.get('DASHSCOPE_API_KEY') or os.environ.get('EVALSCOPE_API_KEY')
|
|
N = 3 # Number of calls for serial/concurrent comparison
|
|
PROMPT = "Reply with only the word 'hello'"
|
|
CONCURRENCY_RATIO = 0.7 # T_concurrent should be < T_serial * this ratio
|
|
|
|
|
|
def _build_input():
|
|
"""Build a simple chat input list."""
|
|
return [ChatMessageUser(content=PROMPT)]
|
|
|
|
|
|
def _default_config():
|
|
"""Build a default generation config for testing."""
|
|
return GenerateConfig(temperature=0.0, max_tokens=32)
|
|
|
|
|
|
def _validate_output(test_case: unittest.TestCase, output):
|
|
"""Assert that a ModelOutput is valid and contains a message."""
|
|
test_case.assertIsNotNone(output, 'generate returned None')
|
|
test_case.assertIsInstance(output, ModelOutput, f'Expected ModelOutput, got {type(output)}')
|
|
test_case.assertFalse(output.empty, 'ModelOutput has no choices')
|
|
test_case.assertIsNotNone(output.message, 'ModelOutput.message is None')
|
|
test_case.assertTrue(output.message.text, 'ModelOutput.message.text is empty')
|
|
|
|
|
|
class TestGenerateAsync(unittest.IsolatedAsyncioTestCase):
|
|
"""End-to-end tests for ModelAPI.generate_async()."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not API_KEY:
|
|
raise unittest.SkipTest('DASHSCOPE_API_KEY or EVALSCOPE_API_KEY not set, skipping real API test.')
|
|
cls.model = OpenAICompatibleAPI(
|
|
model_name=MODEL_NAME,
|
|
base_url=BASE_URL,
|
|
api_key=API_KEY,
|
|
config=GenerateConfig(temperature=0.0, max_tokens=32),
|
|
)
|
|
|
|
async def test_generate_async_concurrency(self):
|
|
"""Verify that generate_async enables true concurrency.
|
|
|
|
Strategy:
|
|
1. Call generate_async N times serially, record total time T_serial.
|
|
2. Call generate_async N times concurrently via asyncio.gather, record T_concurrent.
|
|
3. Assert T_concurrent < T_serial * CONCURRENCY_RATIO.
|
|
"""
|
|
input_messages = _build_input()
|
|
config = _default_config()
|
|
|
|
# --- Serial execution ---
|
|
t0 = time.monotonic()
|
|
serial_outputs = []
|
|
for _ in range(N):
|
|
output = await self.model.generate_async(
|
|
input=input_messages,
|
|
tools=[],
|
|
tool_choice='none',
|
|
config=config,
|
|
)
|
|
serial_outputs.append(output)
|
|
t_serial = time.monotonic() - t0
|
|
|
|
# Validate all serial outputs
|
|
for output in serial_outputs:
|
|
_validate_output(self, output)
|
|
|
|
# --- Concurrent execution ---
|
|
t0 = time.monotonic()
|
|
concurrent_outputs = await asyncio.gather(*[
|
|
self.model.generate_async(
|
|
input=input_messages,
|
|
tools=[],
|
|
tool_choice='none',
|
|
config=config,
|
|
)
|
|
for _ in range(N)
|
|
])
|
|
t_concurrent = time.monotonic() - t0
|
|
|
|
# Validate all concurrent outputs
|
|
for output in concurrent_outputs:
|
|
_validate_output(self, output)
|
|
|
|
# --- Timing comparison ---
|
|
print(f'\n{"=" * 60}')
|
|
print(f' Serial ({N} calls): {t_serial:.3f}s')
|
|
print(f' Concurrent ({N} calls): {t_concurrent:.3f}s')
|
|
print(f' Speedup ratio: {t_serial / t_concurrent:.2f}x')
|
|
print(f' Threshold: T_concurrent < T_serial * {CONCURRENCY_RATIO}')
|
|
print(f' Result: {t_concurrent:.3f}s < {t_serial * CONCURRENCY_RATIO:.3f}s = {"PASS" if t_concurrent < t_serial * CONCURRENCY_RATIO else "FAIL"}')
|
|
print(f'{"=" * 60}')
|
|
|
|
self.assertLess(
|
|
t_concurrent,
|
|
t_serial * CONCURRENCY_RATIO,
|
|
f'Concurrent execution ({t_concurrent:.3f}s) was not significantly faster '
|
|
f'than serial ({t_serial:.3f}s). Expected < {t_serial * CONCURRENCY_RATIO:.3f}s. '
|
|
f'This suggests generate_async is not truly concurrent.',
|
|
)
|
|
|
|
async def test_generate_async_output_structure(self):
|
|
"""Verify that generate_async returns the same ModelOutput structure as generate."""
|
|
input_messages = _build_input()
|
|
config = _default_config()
|
|
|
|
# Async call
|
|
async_output = await self.model.generate_async(
|
|
input=input_messages,
|
|
tools=[],
|
|
tool_choice='none',
|
|
config=config,
|
|
)
|
|
|
|
# Sync call
|
|
sync_output = self.model.generate(
|
|
input=input_messages,
|
|
tools=[],
|
|
tool_choice='none',
|
|
config=config,
|
|
)
|
|
|
|
# Both should be valid ModelOutput instances
|
|
_validate_output(self, async_output)
|
|
_validate_output(self, sync_output)
|
|
|
|
# Structural equivalence checks
|
|
self.assertIs(
|
|
type(async_output),
|
|
type(sync_output),
|
|
f'Type mismatch: async={type(async_output)}, sync={type(sync_output)}',
|
|
)
|
|
self.assertEqual(
|
|
async_output.model,
|
|
sync_output.model,
|
|
f'Model name mismatch: async={async_output.model}, sync={sync_output.model}',
|
|
)
|
|
# Both should have usage info
|
|
self.assertIsNotNone(async_output.usage, 'Async output missing usage')
|
|
self.assertIsNotNone(sync_output.usage, 'Sync output missing usage')
|
|
# Both should have timing info
|
|
self.assertIsNotNone(async_output.time, 'Async output missing time')
|
|
self.assertGreater(async_output.time, 0, 'Async output time should be > 0')
|
|
self.assertIsNotNone(sync_output.time, 'Sync output missing time')
|
|
self.assertGreater(sync_output.time, 0, 'Sync output time should be > 0')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|