|
| 1 | +# Copyright 2026 The Kubernetes Authors. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import http.server |
| 16 | +import json |
| 17 | +import pathlib |
| 18 | +import sys |
| 19 | +import threading |
| 20 | +import pytest |
| 21 | + |
| 22 | +from utils.benchmark import run_benchmark_minimal |
| 23 | +from test_prometheus import is_prometheus_available |
| 24 | + |
| 25 | +PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent.resolve() |
| 26 | +MAIN_PY_PATH = PROJECT_ROOT / "inference_perf" / "main.py" |
| 27 | + |
| 28 | + |
| 29 | +class MockHandler(http.server.BaseHTTPRequestHandler): |
| 30 | + metric_name = "vllm:request_success" |
| 31 | + success_count = 0 |
| 32 | + |
| 33 | + @classmethod |
| 34 | + def reset(cls, metric_name: str) -> None: |
| 35 | + cls.metric_name = metric_name |
| 36 | + cls.success_count = 0 |
| 37 | + |
| 38 | + def do_GET(self): |
| 39 | + if self.path == "/health": |
| 40 | + self.send_response(200) |
| 41 | + self.end_headers() |
| 42 | + elif self.path == "/metrics": |
| 43 | + body = ( |
| 44 | + f"# HELP {self.metric_name} Count of successfully processed requests.\n" |
| 45 | + f"# TYPE {self.metric_name} counter\n" |
| 46 | + f'{self.metric_name}{{model_name="facebook/opt-125m"}} {float(MockHandler.success_count)}\n' |
| 47 | + ) |
| 48 | + self.send_response(200) |
| 49 | + self.send_header("Content-Type", "text/plain") |
| 50 | + self.end_headers() |
| 51 | + self.wfile.write(body.encode("utf-8")) |
| 52 | + else: |
| 53 | + self.send_response(404) |
| 54 | + self.end_headers() |
| 55 | + |
| 56 | + def do_POST(self): |
| 57 | + if self.path == "/v1/completions": |
| 58 | + MockHandler.success_count += 1 |
| 59 | + self.send_response(200) |
| 60 | + self.send_header("Content-Type", "application/json") |
| 61 | + self.end_headers() |
| 62 | + response = { |
| 63 | + "id": "cmpl-mock", |
| 64 | + "object": "text_completion", |
| 65 | + "created": 12345, |
| 66 | + "model": "facebook/opt-125m", |
| 67 | + "choices": [{"text": " mock response", "finish_reason": "length"}], |
| 68 | + "usage": {"prompt_tokens": 1, "total_tokens": 5, "completion_tokens": 4}, |
| 69 | + } |
| 70 | + self.wfile.write(json.dumps(response).encode("utf-8")) |
| 71 | + else: |
| 72 | + self.send_response(404) |
| 73 | + self.end_headers() |
| 74 | + |
| 75 | + def log_message(self, format, *args): |
| 76 | + return |
| 77 | + |
| 78 | + |
| 79 | +def start_mock_server(port: int, metric_name: str) -> http.server.HTTPServer: |
| 80 | + MockHandler.reset(metric_name) |
| 81 | + server = http.server.HTTPServer(("127.0.0.1", port), MockHandler) |
| 82 | + thread = threading.Thread(target=server.serve_forever) |
| 83 | + thread.daemon = True |
| 84 | + thread.start() |
| 85 | + return server |
| 86 | + |
| 87 | + |
| 88 | +def _benchmark_config(prometheus_url: str, port: int) -> dict: |
| 89 | + return { |
| 90 | + "data": { |
| 91 | + "type": "shared_prefix", |
| 92 | + "shared_prefix": { |
| 93 | + "num_groups": 1, |
| 94 | + "num_prompts_per_group": 5, |
| 95 | + "system_prompt_len": 10, |
| 96 | + "question_len": 10, |
| 97 | + "output_len": 10, |
| 98 | + }, |
| 99 | + }, |
| 100 | + "load": { |
| 101 | + "type": "constant", |
| 102 | + "stages": [{"rate": 1, "duration": 15}], |
| 103 | + "num_workers": 1, |
| 104 | + }, |
| 105 | + "api": { |
| 106 | + "type": "completion", |
| 107 | + "streaming": True, |
| 108 | + }, |
| 109 | + "server": { |
| 110 | + "type": "vllm", |
| 111 | + "model_name": "facebook/opt-125m", |
| 112 | + "base_url": f"http://127.0.0.1:{port}", |
| 113 | + "ignore_eos": True, |
| 114 | + }, |
| 115 | + "tokenizer": { |
| 116 | + "pretrained_model_name_or_path": "facebook/opt-125m", |
| 117 | + }, |
| 118 | + "metrics": { |
| 119 | + "type": "prometheus", |
| 120 | + "prometheus": { |
| 121 | + "url": prometheus_url, |
| 122 | + "scrape_interval": 5, |
| 123 | + }, |
| 124 | + }, |
| 125 | + "report": { |
| 126 | + "prometheus": { |
| 127 | + "summary": True, |
| 128 | + }, |
| 129 | + }, |
| 130 | + } |
| 131 | + |
| 132 | + |
| 133 | +@pytest.mark.asyncio |
| 134 | +@pytest.mark.skipif(not is_prometheus_available(), reason="local environment missing prometheus") |
| 135 | +async def test_legacy_metric_name(prometheus_server): |
| 136 | + """Verifies that inference-perf can collect metrics using the legacy name 'vllm:request_success'.""" |
| 137 | + server = start_mock_server(18000, "vllm:request_success") |
| 138 | + |
| 139 | + try: |
| 140 | + result = await run_benchmark_minimal( |
| 141 | + _benchmark_config(prometheus_server, 18000), |
| 142 | + executable=[sys.executable, str(MAIN_PY_PATH)], |
| 143 | + extra_env={"PYTHONPATH": str(PROJECT_ROOT)}, |
| 144 | + ) |
| 145 | + |
| 146 | + assert result.success, f"Benchmark failed: {result.stdout}" |
| 147 | + assert "summary_prometheus_metrics.json" in result.reports |
| 148 | + report = result.reports["summary_prometheus_metrics.json"] |
| 149 | + assert "successes" in report |
| 150 | + success_count = report["successes"]["request_success_count"] |
| 151 | + assert success_count > 0, f"Expected non-zero success count from mock, got {success_count}" |
| 152 | + |
| 153 | + finally: |
| 154 | + server.shutdown() |
| 155 | + server.server_close() |
| 156 | + |
| 157 | + |
| 158 | +@pytest.mark.asyncio |
| 159 | +@pytest.mark.skipif(not is_prometheus_available(), reason="local environment missing prometheus") |
| 160 | +async def test_new_metric_name(prometheus_server): |
| 161 | + """Verifies that inference-perf can collect metrics using the new name 'vllm:request_success_total'.""" |
| 162 | + server = start_mock_server(18001, "vllm:request_success_total") |
| 163 | + |
| 164 | + try: |
| 165 | + result = await run_benchmark_minimal( |
| 166 | + _benchmark_config(prometheus_server, 18001), |
| 167 | + executable=[sys.executable, str(MAIN_PY_PATH)], |
| 168 | + extra_env={"PYTHONPATH": str(PROJECT_ROOT)}, |
| 169 | + ) |
| 170 | + |
| 171 | + assert result.success, f"Benchmark failed: {result.stdout}" |
| 172 | + assert "summary_prometheus_metrics.json" in result.reports |
| 173 | + report = result.reports["summary_prometheus_metrics.json"] |
| 174 | + assert "successes" in report |
| 175 | + success_count = report["successes"]["request_success_count"] |
| 176 | + assert success_count > 0, f"Expected non-zero success count from mock, got {success_count}" |
| 177 | + |
| 178 | + finally: |
| 179 | + server.shutdown() |
| 180 | + server.server_close() |
0 commit comments