Files
jundot--omlx/tests/test_accuracy_benchmark.py
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

494 lines
17 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Unit tests for accuracy benchmark orchestration."""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from omlx.admin.accuracy_benchmark import (
VALID_BENCHMARKS,
AccuracyBenchmarkRequest,
AccuracyBenchmarkRun,
_accumulated_results,
add_to_queue,
cleanup_old_runs,
create_run,
get_accumulated_results,
get_queue_status,
get_run,
reset_accumulated_results,
run_accuracy_benchmark,
)
from omlx.model_settings import ModelSettings
class TestAccuracyBenchmarkRequest:
def test_valid_request(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 300, "gsm8k": 100},
)
assert req.model_id == "test-model"
assert "mmlu" in req.benchmarks
assert req.benchmarks["gsm8k"] == 100
def test_full_dataset_size_zero(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 0},
)
assert req.benchmarks["mmlu"] == 0
def test_empty_benchmarks_rejected(self):
with pytest.raises(Exception):
AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={},
)
def test_invalid_benchmark_rejected(self):
with pytest.raises(Exception):
AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"invalid_bench": 100},
)
def test_all_valid_benchmarks(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={b: 100 for b in VALID_BENCHMARKS},
)
assert len(req.benchmarks) == len(VALID_BENCHMARKS)
def test_enable_thinking_default_false(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
assert req.enable_thinking is False
def test_enable_thinking_true(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
enable_thinking=True,
)
assert req.enable_thinking is True
def test_sampling_profile_default_deterministic(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
assert req.sampling_profile == "deterministic"
def test_sampling_profile_model_settings_accepted(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
sampling_profile="model_settings",
)
assert req.sampling_profile == "model_settings"
def test_sampling_profile_invalid_rejected(self):
with pytest.raises(Exception):
AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
sampling_profile="wild",
)
class TestQueueAndResults:
def setup_method(self):
from omlx.admin.accuracy_benchmark import _queue
_queue.clear()
reset_accumulated_results()
def test_add_to_queue(self):
req = AccuracyBenchmarkRequest(
model_id="model-a",
benchmarks={"mmlu": 100},
)
add_to_queue(req)
status = get_queue_status()
assert len(status["queue"]) == 1
assert status["queue"][0]["model_id"] == "model-a"
def test_queue_status_empty(self):
status = get_queue_status()
assert status["running"] is False
assert len(status["queue"]) == 0
def test_accumulated_results(self):
_accumulated_results.append({"model_id": "m1", "benchmark": "mmlu", "accuracy": 0.5})
results = get_accumulated_results()
assert len(results) == 1
assert results[0]["model_id"] == "m1"
def test_reset_accumulated_results(self):
_accumulated_results.append({"model_id": "m1", "benchmark": "mmlu", "accuracy": 0.5})
reset_accumulated_results()
assert len(get_accumulated_results()) == 0
class TestRunLifecycle:
def setup_method(self):
from omlx.admin.accuracy_benchmark import _accuracy_runs
_accuracy_runs.clear()
def test_create_run(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run = create_run(req)
assert run.bench_id is not None
assert run.status == "running"
assert run.request == req
def test_get_run(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run = create_run(req)
found = get_run(run.bench_id)
assert found is run
def test_get_run_not_found(self):
assert get_run("nonexistent") is None
def test_cleanup_old_runs(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run1 = create_run(req)
run2 = create_run(req)
run1.status = "completed"
run2.status = "running"
cleanup_old_runs()
assert get_run(run1.bench_id) is None
assert get_run(run2.bench_id) is run2
def test_cleanup_error_runs(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run = create_run(req)
run.status = "error"
cleanup_old_runs()
assert get_run(run.bench_id) is None
class TestRunAccuracyBenchmark:
@pytest.mark.asyncio
async def test_sends_done_event(self):
"""Verify that a successful run sends a done event."""
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run = create_run(req)
# Mock engine_pool
mock_engine = AsyncMock()
mock_engine.chat = AsyncMock(return_value=MagicMock(text="A"))
mock_pool = MagicMock()
mock_pool.get_loaded_model_ids = MagicMock(return_value=[])
mock_pool.get_engine = AsyncMock(return_value=mock_engine)
mock_pool._unload_engine = AsyncMock()
# Mock evaluator
mock_result = MagicMock()
mock_result.benchmark_name = "mmlu"
mock_result.accuracy = 0.75
mock_result.total_questions = 4
mock_result.correct_count = 3
mock_result.time_seconds = 1.0
mock_result.category_scores = None
mock_result.thinking_used = False
mock_evaluator = MagicMock()
mock_evaluator.load_dataset = AsyncMock(return_value=[{"id": "1"}])
mock_evaluator.run = AsyncMock(return_value=mock_result)
mock_bench_cls = MagicMock(return_value=mock_evaluator)
with patch.dict("omlx.eval.BENCHMARKS", {"mmlu": mock_bench_cls}, clear=True):
await run_accuracy_benchmark(run, mock_pool)
# Collect all events from the replay log.
events = list(run.events)
event_types = [e["type"] for e in events]
assert "done" in event_types
assert run.status == "completed"
@pytest.mark.asyncio
async def test_cancellation(self):
"""Verify that cancelling stops the run."""
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 100},
)
run = create_run(req)
run.status = "cancelled" # Pre-cancel
mock_pool = MagicMock()
mock_pool.get_loaded_model_ids = MagicMock(return_value=[])
mock_pool.get_engine = AsyncMock(return_value=MagicMock())
mock_pool._unload_engine = AsyncMock()
mock_evaluator = MagicMock()
mock_evaluator.load_dataset = AsyncMock(return_value=[])
mock_evaluator.run = AsyncMock(return_value=MagicMock(
benchmark_name="mmlu",
accuracy=0.0,
total_questions=0,
correct_count=0,
time_seconds=0.0,
category_scores=None,
))
mock_bench_cls = MagicMock(return_value=mock_evaluator)
with patch.dict("omlx.eval.BENCHMARKS", {"mmlu": mock_bench_cls}):
await run_accuracy_benchmark(run, mock_pool)
# Should have stopped early
assert len(run.results) == 0
class TestSamplingProfile:
"""sampling_profile gates whether per-model sampling reaches the evaluator.
Default "deterministic" must read nothing (reproducible greedy scores);
"model_settings" must forward the model's configured sampling. See #606 /
the #1254 deterministic-default request.
"""
def _mock_pool(self, model_settings):
mock_engine = AsyncMock()
mock_engine.chat = AsyncMock(return_value=MagicMock(text="A"))
mock_pool = MagicMock()
mock_pool.get_loaded_model_ids = MagicMock(return_value=[])
mock_pool.get_engine = AsyncMock(return_value=mock_engine)
mock_pool._unload_engine = AsyncMock()
mock_pool._settings_manager.get_settings = MagicMock(return_value=model_settings)
return mock_pool
async def _captured_sampling_kwargs(self, req, mock_pool):
run = create_run(req)
mock_result = MagicMock(
benchmark_name="mmlu", accuracy=0.5, total_questions=1,
correct_count=1, time_seconds=0.1, category_scores=None,
thinking_used=False,
)
mock_evaluator = MagicMock()
mock_evaluator.load_dataset = AsyncMock(return_value=[{"id": "1"}])
mock_evaluator.run = AsyncMock(return_value=mock_result)
mock_bench_cls = MagicMock(return_value=mock_evaluator)
with patch.dict("omlx.eval.BENCHMARKS", {"mmlu": mock_bench_cls}, clear=True):
await run_accuracy_benchmark(run, mock_pool)
return mock_evaluator.run.call_args.kwargs["sampling_kwargs"]
@pytest.mark.asyncio
async def test_deterministic_ignores_model_settings(self):
# Default profile is "deterministic".
req = AccuracyBenchmarkRequest(model_id="test-model", benchmarks={"mmlu": 1})
mock_pool = self._mock_pool(ModelSettings(temperature=0.9, top_p=0.95))
assert await self._captured_sampling_kwargs(req, mock_pool) == {}
@pytest.mark.asyncio
async def test_model_settings_forwards_sampling(self):
req = AccuracyBenchmarkRequest(
model_id="test-model",
benchmarks={"mmlu": 1},
sampling_profile="model_settings",
)
mock_pool = self._mock_pool(ModelSettings(temperature=0.9, top_p=0.95))
sampling_kwargs = await self._captured_sampling_kwargs(req, mock_pool)
assert sampling_kwargs["temperature"] == 0.9
assert sampling_kwargs["top_p"] == 0.95
@pytest.mark.asyncio
async def test_deterministic_keeps_chat_template_kwargs(self):
# Template kwargs are prompt construction, not sampling — forwarded
# even under the deterministic profile.
req = AccuracyBenchmarkRequest(model_id="test-model", benchmarks={"mmlu": 1})
mock_pool = self._mock_pool(
ModelSettings(temperature=0.9, chat_template_kwargs={"custom_flag": True})
)
sampling_kwargs = await self._captured_sampling_kwargs(req, mock_pool)
assert sampling_kwargs == {"chat_template_kwargs": {"custom_flag": True}}
# =============================================================================
# External endpoint accuracy benchmark tests
# =============================================================================
def _external_dict():
return {
"base_url": "http://localhost:8001/v1",
"api_key": "sk-test",
"model": "remote-model",
}
class TestExternalAccuracyRequest:
def test_external_accepted(self):
req = AccuracyBenchmarkRequest(
model_id="remote-model",
benchmarks={"mmlu": 100},
external=_external_dict(),
)
assert req.external is not None
assert req.external.model == "remote-model"
def test_external_forces_thinking_off(self):
req = AccuracyBenchmarkRequest(
model_id="remote-model",
benchmarks={"mmlu": 100},
enable_thinking=True,
external=_external_dict(),
)
assert req.enable_thinking is False
def test_local_keeps_thinking(self):
req = AccuracyBenchmarkRequest(
model_id="local-model",
benchmarks={"mmlu": 100},
enable_thinking=True,
)
assert req.enable_thinking is True
def test_queue_status_flags_external(self):
req = AccuracyBenchmarkRequest(
model_id="remote-model",
benchmarks={"mmlu": 100},
external=_external_dict(),
)
add_to_queue(req)
try:
entry = get_queue_status()["queue"][-1]
assert entry["external"] is True
finally:
from omlx.admin.accuracy_benchmark import _queue
_queue.clear()
class TestExternalAccuracyRun:
def _mock_result(self):
return MagicMock(
benchmark_name="mmlu",
accuracy=0.5,
total_questions=2,
correct_count=1,
time_seconds=0.1,
category_scores=None,
thinking_used=False,
question_results=[],
)
def _mock_evaluator(self):
mock_evaluator = MagicMock()
mock_evaluator.load_dataset = AsyncMock(return_value=[{"id": "1"}])
mock_evaluator.run = AsyncMock(return_value=self._mock_result())
return mock_evaluator
def _external_request(self):
return AccuracyBenchmarkRequest(
model_id="remote-model",
benchmarks={"mmlu": 100},
batch_size=4,
external=_external_dict(),
)
@pytest.mark.asyncio
async def test_external_run_uses_adapter_and_skips_pool(self):
run = create_run(self._external_request())
mock_pool = MagicMock()
mock_evaluator = self._mock_evaluator()
mock_bench_cls = MagicMock(return_value=mock_evaluator)
mock_adapter = MagicMock()
mock_adapter.preflight = AsyncMock()
mock_client = MagicMock()
mock_client.aclose = AsyncMock()
with (
patch.dict("omlx.eval.BENCHMARKS", {"mmlu": mock_bench_cls}, clear=True),
patch(
"omlx.admin.accuracy_benchmark.ExternalAPIClient",
return_value=mock_client,
),
patch(
"omlx.admin.accuracy_benchmark.ExternalChatAdapter",
return_value=mock_adapter,
) as adapter_cls,
):
await run_accuracy_benchmark(run, mock_pool)
assert run.status == "completed"
mock_pool.get_engine.assert_not_called()
mock_pool.get_loaded_model_ids.assert_not_called()
mock_pool._unload_engine.assert_not_called()
mock_adapter.preflight.assert_awaited_once()
adapter_cls.assert_called_once_with(mock_client, "deterministic")
# Evaluator got the adapter, empty sampling kwargs, thinking off
call = mock_evaluator.run.call_args
assert call.args[0] is mock_adapter
assert call.kwargs["sampling_kwargs"] == {}
assert call.kwargs["enable_thinking"] is False
assert call.kwargs["batch_size"] == 4
# Result carries the external flag and the remote model name
assert run.results[0]["external"] is True
assert run.results[0]["model_id"] == "remote-model"
mock_client.aclose.assert_awaited()
# Clean up accumulated results this test appended
reset_accumulated_results()
@pytest.mark.asyncio
async def test_external_preflight_failure_emits_error(self):
from omlx.admin.external_api import ExternalEndpointError
run = create_run(self._external_request())
mock_pool = MagicMock()
mock_adapter = MagicMock()
mock_adapter.preflight = AsyncMock(
side_effect=ExternalEndpointError(
"External endpoint rejected the API key (HTTP 401)"
)
)
mock_client = MagicMock()
mock_client.aclose = AsyncMock()
with (
patch(
"omlx.admin.accuracy_benchmark.ExternalAPIClient",
return_value=mock_client,
),
patch(
"omlx.admin.accuracy_benchmark.ExternalChatAdapter",
return_value=mock_adapter,
),
):
await run_accuracy_benchmark(run, mock_pool)
assert run.status == "error"
assert "rejected the API key" in run.error_message
error_events = [e for e in run.events if e["type"] == "error"]
assert error_events
mock_client.aclose.assert_awaited()