latencyops 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- latencyops/__init__.py +33 -0
- latencyops/adapters.py +186 -0
- latencyops/benchmark.py +64 -0
- latencyops/benchmark_suite.py +112 -0
- latencyops/capabilities.py +66 -0
- latencyops/cli.py +54 -0
- latencyops/comparison.py +52 -0
- latencyops/config.py +71 -0
- latencyops/controller.py +85 -0
- latencyops/experiments.py +48 -0
- latencyops/gateway.py +149 -0
- latencyops/metrics.py +52 -0
- latencyops/models.py +152 -0
- latencyops/orchestrators.py +131 -0
- latencyops/policy.py +142 -0
- latencyops/router.py +58 -0
- latencyops/scheduling.py +77 -0
- latencyops/signals.py +35 -0
- latencyops/telemetry.py +82 -0
- latencyops-0.1.0.dist-info/METADATA +346 -0
- latencyops-0.1.0.dist-info/RECORD +25 -0
- latencyops-0.1.0.dist-info/WHEEL +5 -0
- latencyops-0.1.0.dist-info/entry_points.txt +2 -0
- latencyops-0.1.0.dist-info/licenses/LICENSE +15 -0
- latencyops-0.1.0.dist-info/top_level.txt +1 -0
latencyops/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LatencyOps: latency measurement and quality-aware inference planning."""
|
|
2
|
+
|
|
3
|
+
from .capabilities import EnforcedPlan, ProviderCapabilities, enforce_plan
|
|
4
|
+
from .controller import ProactiveController
|
|
5
|
+
from .gateway import GatewayService, create_server
|
|
6
|
+
from .models import LatencyContract, RequestProfile, StreamChunk, StreamingResult
|
|
7
|
+
from .policy import AdaptiveProviderSelector, ElasticInferencePlanner
|
|
8
|
+
from .orchestrators import PrometheusMetricsCollector, SGLangMetricsCollector, TensorRTLLMMetricsCollector, VLLMMetricsCollector
|
|
9
|
+
from .signals import RuntimeSignalNormalizer
|
|
10
|
+
from .scheduling import CachePressureSignal, InMemoryRequestQueue, PriorityScheduler
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AdaptiveProviderSelector",
|
|
14
|
+
"CachePressureSignal",
|
|
15
|
+
"ElasticInferencePlanner",
|
|
16
|
+
"EnforcedPlan",
|
|
17
|
+
"GatewayService",
|
|
18
|
+
"InMemoryRequestQueue",
|
|
19
|
+
"LatencyContract",
|
|
20
|
+
"PriorityScheduler",
|
|
21
|
+
"PrometheusMetricsCollector",
|
|
22
|
+
"ProactiveController",
|
|
23
|
+
"ProviderCapabilities",
|
|
24
|
+
"RequestProfile",
|
|
25
|
+
"RuntimeSignalNormalizer",
|
|
26
|
+
"SGLangMetricsCollector",
|
|
27
|
+
"TensorRTLLMMetricsCollector",
|
|
28
|
+
"VLLMMetricsCollector",
|
|
29
|
+
"StreamChunk",
|
|
30
|
+
"StreamingResult",
|
|
31
|
+
"create_server",
|
|
32
|
+
"enforce_plan",
|
|
33
|
+
]
|
latencyops/adapters.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Provider adapters built on the standard library HTTP client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable, Iterable, Mapping
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
from .capabilities import ProviderCapabilities
|
|
10
|
+
from .models import InferencePlan, StreamChunk
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderError(RuntimeError):
|
|
14
|
+
"""Normalized provider failure without including request content."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProviderHealth:
|
|
19
|
+
"""Result of a provider health probe."""
|
|
20
|
+
|
|
21
|
+
healthy: bool
|
|
22
|
+
detail: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CallableProvider:
|
|
26
|
+
"""Wrap a local callable as a deterministic inference provider."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str, responder: Callable[[str, InferencePlan], str]):
|
|
29
|
+
self.name = name
|
|
30
|
+
self.responder = responder
|
|
31
|
+
|
|
32
|
+
def generate(self, prompt: str, plan: InferencePlan) -> str:
|
|
33
|
+
return self.responder(prompt, plan)
|
|
34
|
+
|
|
35
|
+
def stream(self, prompt: str, plan: InferencePlan) -> Iterable[StreamChunk]:
|
|
36
|
+
yield StreamChunk(self.generate(prompt, plan))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OpenAICompatibleProvider:
|
|
40
|
+
"""Minimal adapter for JSON-compatible OpenAI-style completion endpoints."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
name: str,
|
|
45
|
+
endpoint: str,
|
|
46
|
+
api_key: str | None = None,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
model_names: Mapping[str, str] | None = None,
|
|
49
|
+
health_endpoint: str | None = None,
|
|
50
|
+
):
|
|
51
|
+
self.name = name
|
|
52
|
+
self.endpoint = endpoint
|
|
53
|
+
self.api_key = api_key
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
self.model_names = dict(model_names or {})
|
|
56
|
+
self.health_endpoint = health_endpoint
|
|
57
|
+
self.capabilities = ProviderCapabilities()
|
|
58
|
+
|
|
59
|
+
def _model_name(self, plan: InferencePlan) -> str:
|
|
60
|
+
return self.model_names.get(plan.model_tier, plan.model_tier)
|
|
61
|
+
|
|
62
|
+
def _headers(self, streaming: bool = False) -> dict[str, str]:
|
|
63
|
+
headers = {"Content-Type": "application/json"}
|
|
64
|
+
if streaming:
|
|
65
|
+
headers["Accept"] = "text/event-stream"
|
|
66
|
+
if self.api_key:
|
|
67
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
68
|
+
return headers
|
|
69
|
+
|
|
70
|
+
def generate(self, prompt: str, plan: InferencePlan) -> str:
|
|
71
|
+
payload = json.dumps({"model": self._model_name(plan), "prompt": prompt, "stream": False}).encode()
|
|
72
|
+
request = Request(self.endpoint, data=payload, headers=self._headers(), method="POST")
|
|
73
|
+
try:
|
|
74
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
75
|
+
body = json.loads(response.read().decode())
|
|
76
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
|
|
77
|
+
raise ProviderError(f"provider {self.name!r} request failed") from exc
|
|
78
|
+
choices = body.get("choices", [])
|
|
79
|
+
if not choices or "text" not in choices[0]:
|
|
80
|
+
raise ProviderError(f"provider {self.name!r} returned an invalid response")
|
|
81
|
+
return choices[0]["text"]
|
|
82
|
+
|
|
83
|
+
def stream(self, prompt: str, plan: InferencePlan) -> Iterable[StreamChunk]:
|
|
84
|
+
payload = json.dumps({"model": self._model_name(plan), "prompt": prompt, "stream": True}).encode()
|
|
85
|
+
request = Request(self.endpoint, data=payload, headers=self._headers(streaming=True), method="POST")
|
|
86
|
+
try:
|
|
87
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
88
|
+
for raw_line in response:
|
|
89
|
+
line = raw_line.decode().strip()
|
|
90
|
+
if not line or not line.startswith("data:"):
|
|
91
|
+
continue
|
|
92
|
+
data = line[5:].strip()
|
|
93
|
+
if data == "[DONE]":
|
|
94
|
+
break
|
|
95
|
+
event = json.loads(data)
|
|
96
|
+
text = event.get("choices", [{}])[0].get("text", "")
|
|
97
|
+
if text:
|
|
98
|
+
yield StreamChunk(text)
|
|
99
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
|
|
100
|
+
raise ProviderError(f"provider {self.name!r} stream failed") from exc
|
|
101
|
+
|
|
102
|
+
def health(self) -> ProviderHealth:
|
|
103
|
+
if not self.health_endpoint:
|
|
104
|
+
return ProviderHealth(True, "health probe not configured")
|
|
105
|
+
request = Request(self.health_endpoint, headers=self._headers(), method="GET")
|
|
106
|
+
try:
|
|
107
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
108
|
+
return ProviderHealth(200 <= response.status < 300, "HTTP health probe completed")
|
|
109
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError) as exc:
|
|
110
|
+
return ProviderHealth(False, "HTTP health probe failed")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class OpenAIChatCompatibleProvider(OpenAICompatibleProvider):
|
|
114
|
+
"""Adapter for OpenAI-compatible chat-completion endpoints."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, *args, chat_template_kwargs: Mapping[str, object] | None = None, **kwargs):
|
|
117
|
+
super().__init__(*args, **kwargs)
|
|
118
|
+
self.chat_template_kwargs = dict(chat_template_kwargs or {})
|
|
119
|
+
|
|
120
|
+
def _payload(self, prompt: str, plan: InferencePlan, streaming: bool) -> dict[str, object]:
|
|
121
|
+
payload: dict[str, object] = {
|
|
122
|
+
"model": self._model_name(plan),
|
|
123
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
124
|
+
"stream": streaming,
|
|
125
|
+
}
|
|
126
|
+
if self.chat_template_kwargs:
|
|
127
|
+
payload["chat_template_kwargs"] = self.chat_template_kwargs
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
def generate(self, prompt: str, plan: InferencePlan) -> str:
|
|
131
|
+
request = Request(
|
|
132
|
+
self.endpoint,
|
|
133
|
+
data=json.dumps(self._payload(prompt, plan, False)).encode(),
|
|
134
|
+
headers=self._headers(),
|
|
135
|
+
method="POST",
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
139
|
+
body = json.loads(response.read().decode())
|
|
140
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
|
|
141
|
+
raise ProviderError(f"provider {self.name!r} request failed") from exc
|
|
142
|
+
choices = body.get("choices", [])
|
|
143
|
+
content = choices[0].get("message", {}).get("content") if choices else None
|
|
144
|
+
if not isinstance(content, str):
|
|
145
|
+
raise ProviderError(f"provider {self.name!r} returned an invalid chat response")
|
|
146
|
+
return content
|
|
147
|
+
|
|
148
|
+
def stream(self, prompt: str, plan: InferencePlan) -> Iterable[StreamChunk]:
|
|
149
|
+
request = Request(
|
|
150
|
+
self.endpoint,
|
|
151
|
+
data=json.dumps(self._payload(prompt, plan, True)).encode(),
|
|
152
|
+
headers=self._headers(streaming=True),
|
|
153
|
+
method="POST",
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
157
|
+
for raw_line in response:
|
|
158
|
+
line = raw_line.decode().strip()
|
|
159
|
+
if not line or not line.startswith("data:"):
|
|
160
|
+
continue
|
|
161
|
+
data = line[5:].strip()
|
|
162
|
+
if data == "[DONE]":
|
|
163
|
+
break
|
|
164
|
+
event = json.loads(data)
|
|
165
|
+
choice = event.get("choices", [{}])[0]
|
|
166
|
+
delta = choice.get("delta", {})
|
|
167
|
+
text = delta.get("content", "")
|
|
168
|
+
if text:
|
|
169
|
+
yield StreamChunk(text)
|
|
170
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
|
|
171
|
+
raise ProviderError(f"provider {self.name!r} chat stream failed") from exc
|
|
172
|
+
|
|
173
|
+
def list_models(self) -> list[str]:
|
|
174
|
+
"""Return model IDs visible to this provider credential."""
|
|
175
|
+
endpoint = self.endpoint.rsplit("/v1/", 1)[0] + "/v1/models"
|
|
176
|
+
request = Request(endpoint, headers=self._headers(), method="GET")
|
|
177
|
+
try:
|
|
178
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
179
|
+
body = json.loads(response.read().decode())
|
|
180
|
+
except (HTTPError, URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
|
|
181
|
+
raise ProviderError(f"provider {self.name!r} model listing failed") from exc
|
|
182
|
+
return [item["id"] for item in body.get("data", []) if isinstance(item.get("id"), str)]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class VLLMCompatibleProvider(OpenAIChatCompatibleProvider):
|
|
186
|
+
"""vLLM adapter using its OpenAI-compatible chat-completions endpoint."""
|
latencyops/benchmark.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Small benchmark runner for provider comparisons."""
|
|
2
|
+
|
|
3
|
+
from time import perf_counter
|
|
4
|
+
from typing import Callable, Iterable
|
|
5
|
+
|
|
6
|
+
from .models import InferencePlan, LatencySample, StreamingResult
|
|
7
|
+
from .router import InferenceProvider, StreamingProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BenchmarkRunner:
|
|
11
|
+
"""Run synthetic or approved prompts without retaining prompt content."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, clock: Callable[[], float] = perf_counter):
|
|
14
|
+
self.clock = clock
|
|
15
|
+
|
|
16
|
+
def run(self, provider: InferenceProvider, prompts: Iterable[str], plan: InferencePlan) -> list[LatencySample]:
|
|
17
|
+
samples: list[LatencySample] = []
|
|
18
|
+
for prompt in prompts:
|
|
19
|
+
started = self.clock()
|
|
20
|
+
try:
|
|
21
|
+
provider.generate(prompt, plan)
|
|
22
|
+
except Exception:
|
|
23
|
+
elapsed_ms = (self.clock() - started) * 1000
|
|
24
|
+
samples.append(LatencySample(elapsed_ms, None, elapsed_ms, success=False))
|
|
25
|
+
continue
|
|
26
|
+
elapsed_ms = (self.clock() - started) * 1000
|
|
27
|
+
samples.append(LatencySample(elapsed_ms, None, elapsed_ms))
|
|
28
|
+
return samples
|
|
29
|
+
|
|
30
|
+
def run_streaming(
|
|
31
|
+
self, provider: StreamingProvider, prompt: str, plan: InferencePlan
|
|
32
|
+
) -> StreamingResult:
|
|
33
|
+
"""Collect one stream and measure TTFT and mean TPOT separately."""
|
|
34
|
+
started = self.clock()
|
|
35
|
+
first_chunk_at: float | None = None
|
|
36
|
+
first_chunk_token_count = 0
|
|
37
|
+
token_count = 0
|
|
38
|
+
text: list[str] = []
|
|
39
|
+
try:
|
|
40
|
+
for chunk in provider.stream(prompt, plan):
|
|
41
|
+
now = self.clock()
|
|
42
|
+
if first_chunk_at is None:
|
|
43
|
+
first_chunk_at = now
|
|
44
|
+
first_chunk_token_count = chunk.token_count
|
|
45
|
+
token_count += chunk.token_count
|
|
46
|
+
text.append(chunk.text)
|
|
47
|
+
except Exception:
|
|
48
|
+
elapsed_ms = (self.clock() - started) * 1000
|
|
49
|
+
return StreamingResult("".join(text), LatencySample(
|
|
50
|
+
(first_chunk_at - started) * 1000 if first_chunk_at is not None else elapsed_ms,
|
|
51
|
+
None,
|
|
52
|
+
elapsed_ms,
|
|
53
|
+
success=False,
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
ended = self.clock()
|
|
57
|
+
elapsed_ms = (ended - started) * 1000
|
|
58
|
+
if first_chunk_at is None:
|
|
59
|
+
return StreamingResult("", LatencySample(elapsed_ms, None, elapsed_ms))
|
|
60
|
+
intervals = max(token_count - first_chunk_token_count, 0)
|
|
61
|
+
tpot_ms = ((ended - first_chunk_at) * 1000 / intervals) if intervals else None
|
|
62
|
+
return StreamingResult("".join(text), LatencySample(
|
|
63
|
+
(first_chunk_at - started) * 1000, tpot_ms, elapsed_ms
|
|
64
|
+
))
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Scenario benchmark runner for proactive policy evaluation."""
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from .benchmark import BenchmarkRunner
|
|
9
|
+
from .models import InferencePlan, LatencyContract, LatencySample, RequestProfile
|
|
10
|
+
from .router import StreamingProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class BenchmarkScenario:
|
|
15
|
+
"""A repeatable workload definition with content-free reporting metadata."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
prompt: str
|
|
19
|
+
contract: LatencyContract
|
|
20
|
+
profile: RequestProfile
|
|
21
|
+
cache_state: str = "unknown"
|
|
22
|
+
network_region: str = "unknown"
|
|
23
|
+
estimated_cost_per_1k_tokens: float | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class BenchmarkConfig:
|
|
28
|
+
"""Execution controls for warm-up, concurrency, and optional rate limiting."""
|
|
29
|
+
|
|
30
|
+
warmup_runs: int = 0
|
|
31
|
+
measured_runs: int = 1
|
|
32
|
+
concurrency: int = 1
|
|
33
|
+
requests_per_second: float | None = None
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if min(self.warmup_runs, self.measured_runs) < 0:
|
|
37
|
+
raise ValueError("run counts cannot be negative")
|
|
38
|
+
if self.concurrency < 1:
|
|
39
|
+
raise ValueError("concurrency must be positive")
|
|
40
|
+
if self.requests_per_second is not None and self.requests_per_second <= 0:
|
|
41
|
+
raise ValueError("requests_per_second must be positive")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ScenarioBenchmarkRunner:
|
|
45
|
+
"""Run identical scenarios concurrently and report contract outcomes."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, clock_runner: BenchmarkRunner | None = None):
|
|
48
|
+
self.runner = clock_runner or BenchmarkRunner()
|
|
49
|
+
|
|
50
|
+
def run(
|
|
51
|
+
self,
|
|
52
|
+
provider: StreamingProvider,
|
|
53
|
+
scenario: BenchmarkScenario,
|
|
54
|
+
plan: InferencePlan,
|
|
55
|
+
config: BenchmarkConfig | None = None,
|
|
56
|
+
quality_evaluator: Callable[[str], float] | None = None,
|
|
57
|
+
) -> dict[str, object]:
|
|
58
|
+
config = config or BenchmarkConfig()
|
|
59
|
+
for _ in range(config.warmup_runs):
|
|
60
|
+
self.runner.run_streaming(provider, scenario.prompt, plan)
|
|
61
|
+
interval = 1 / config.requests_per_second if config.requests_per_second else 0
|
|
62
|
+
|
|
63
|
+
def measure(_: int) -> tuple[LatencySample, float | None]:
|
|
64
|
+
if interval:
|
|
65
|
+
sleep(interval)
|
|
66
|
+
result = self.runner.run_streaming(provider, scenario.prompt, plan)
|
|
67
|
+
quality = quality_evaluator(result.text) if quality_evaluator else None
|
|
68
|
+
return result.sample, quality
|
|
69
|
+
|
|
70
|
+
with ThreadPoolExecutor(max_workers=config.concurrency) as executor:
|
|
71
|
+
measured = list(executor.map(measure, range(config.measured_runs)))
|
|
72
|
+
samples = [item[0] for item in measured]
|
|
73
|
+
summary = self._summary(samples, scenario, measured)
|
|
74
|
+
return summary
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _summary(
|
|
78
|
+
samples: list[LatencySample],
|
|
79
|
+
scenario: BenchmarkScenario,
|
|
80
|
+
measured: list[tuple[LatencySample, float | None]],
|
|
81
|
+
) -> dict[str, object]:
|
|
82
|
+
successful = [sample for sample in samples if sample.success]
|
|
83
|
+
quality_scores = [score for _, score in measured if score is not None]
|
|
84
|
+
deadline_violations = sum(
|
|
85
|
+
sample.success and sample.end_to_end_ms > scenario.contract.deadline_ms
|
|
86
|
+
for sample in samples
|
|
87
|
+
)
|
|
88
|
+
tokens = scenario.profile.prompt_tokens + scenario.profile.expected_output_tokens
|
|
89
|
+
cost = (
|
|
90
|
+
tokens / 1000 * scenario.estimated_cost_per_1k_tokens
|
|
91
|
+
if scenario.estimated_cost_per_1k_tokens is not None else None
|
|
92
|
+
)
|
|
93
|
+
from .metrics import percentile
|
|
94
|
+
return {
|
|
95
|
+
"scenario": scenario.name,
|
|
96
|
+
"runs": len(samples),
|
|
97
|
+
"warmup_excluded": True,
|
|
98
|
+
"concurrency": len(samples),
|
|
99
|
+
"success_rate": len(successful) / len(samples) if samples else 0.0,
|
|
100
|
+
"ttft_p50_ms": percentile([s.ttft_ms for s in samples], 50) if samples else None,
|
|
101
|
+
"ttft_p95_ms": percentile([s.ttft_ms for s in samples], 95) if samples else None,
|
|
102
|
+
"tpot_p50_ms": percentile([s.tpot_ms for s in successful if s.tpot_ms is not None], 50) if any(s.tpot_ms is not None for s in successful) else None,
|
|
103
|
+
"e2e_p50_ms": percentile([s.end_to_end_ms for s in samples], 50) if samples else None,
|
|
104
|
+
"e2e_p95_ms": percentile([s.end_to_end_ms for s in samples], 95) if samples else None,
|
|
105
|
+
"deadline_violations": deadline_violations,
|
|
106
|
+
"quality_mean": sum(quality_scores) / len(quality_scores) if quality_scores else None,
|
|
107
|
+
"quality_floor": scenario.contract.quality_floor,
|
|
108
|
+
"quality_satisfied": all(score >= scenario.contract.quality_floor for score in quality_scores) if quality_scores else None,
|
|
109
|
+
"estimated_cost": cost,
|
|
110
|
+
"cache_state": scenario.cache_state,
|
|
111
|
+
"network_region": scenario.network_region,
|
|
112
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Provider capability negotiation and safe plan enforcement."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .models import InferencePlan, LatencyContract
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ProviderCapabilities:
|
|
10
|
+
"""Execution features a provider can honor."""
|
|
11
|
+
|
|
12
|
+
streaming: bool = True
|
|
13
|
+
model_mapping: bool = True
|
|
14
|
+
precision: bool = False
|
|
15
|
+
context_policy: bool = False
|
|
16
|
+
speculation: bool = False
|
|
17
|
+
early_exit: bool = False
|
|
18
|
+
usage_metadata: bool = False
|
|
19
|
+
queue_metrics: bool = False
|
|
20
|
+
cache_metrics: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class EnforcedPlan:
|
|
25
|
+
"""Requested plan, provider-supported plan, and unapplied choices."""
|
|
26
|
+
|
|
27
|
+
requested: InferencePlan
|
|
28
|
+
enforced: InferencePlan
|
|
29
|
+
unsupported: tuple[str, ...] = ()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def enforce_plan(
|
|
33
|
+
contract: LatencyContract,
|
|
34
|
+
plan: InferencePlan,
|
|
35
|
+
capabilities: ProviderCapabilities,
|
|
36
|
+
) -> EnforcedPlan:
|
|
37
|
+
"""Remove unsupported optimizations while preserving critical safeguards."""
|
|
38
|
+
unsupported: list[str] = []
|
|
39
|
+
precision = plan.precision
|
|
40
|
+
context_policy = plan.context_policy
|
|
41
|
+
speculative_tokens = plan.speculative_tokens
|
|
42
|
+
early_exit = plan.early_exit
|
|
43
|
+
rationale = list(plan.rationale)
|
|
44
|
+
if precision != "fp16" and not capabilities.precision:
|
|
45
|
+
unsupported.append("precision")
|
|
46
|
+
precision = "fp16"
|
|
47
|
+
if context_policy != "full" and not capabilities.context_policy:
|
|
48
|
+
unsupported.append("context_policy")
|
|
49
|
+
context_policy = "full"
|
|
50
|
+
if speculative_tokens and not capabilities.speculation:
|
|
51
|
+
unsupported.append("speculation")
|
|
52
|
+
speculative_tokens = 0
|
|
53
|
+
if early_exit and not capabilities.early_exit:
|
|
54
|
+
unsupported.append("early_exit")
|
|
55
|
+
early_exit = False
|
|
56
|
+
if unsupported:
|
|
57
|
+
rationale.append("provider cannot enforce: " + ", ".join(unsupported))
|
|
58
|
+
enforced = InferencePlan(
|
|
59
|
+
plan.model_tier, precision, context_policy, speculative_tokens, early_exit, tuple(rationale)
|
|
60
|
+
)
|
|
61
|
+
if contract.risk_class == "critical" and enforced != plan:
|
|
62
|
+
enforced = InferencePlan(
|
|
63
|
+
plan.model_tier, "fp16", "full", 0, False,
|
|
64
|
+
tuple(rationale + ["critical safeguards enforced at provider boundary"]),
|
|
65
|
+
)
|
|
66
|
+
return EnforcedPlan(plan, enforced, tuple(unsupported))
|
latencyops/cli.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Command-line entry point for the LatencyOps reference application."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from .config import build_service, load_config
|
|
8
|
+
from .models import LatencyContract, RequestProfile, SystemSignals
|
|
9
|
+
from .policy import ElasticInferencePlanner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="latencyops")
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
15
|
+
gateway = subparsers.add_parser("gateway", help="start the configured HTTP gateway")
|
|
16
|
+
gateway.add_argument("--config", required=True)
|
|
17
|
+
plan = subparsers.add_parser("plan", help="calculate a plan from a JSON request")
|
|
18
|
+
plan.add_argument("--request", required=True)
|
|
19
|
+
health = subparsers.add_parser("health", help="check configured provider health")
|
|
20
|
+
health.add_argument("--config", required=True)
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
if args.command == "plan":
|
|
23
|
+
with open(args.request, encoding="utf-8") as handle:
|
|
24
|
+
request = json.load(handle)
|
|
25
|
+
contract = LatencyContract(
|
|
26
|
+
deadline_ms=float(request.get("deadline_ms", 1000)),
|
|
27
|
+
quality_floor=float(request.get("quality_floor", 0.9)),
|
|
28
|
+
risk_class=request.get("risk_class", "medium"),
|
|
29
|
+
)
|
|
30
|
+
profile = RequestProfile(
|
|
31
|
+
prompt_tokens=int(request.get("prompt_tokens", 0)),
|
|
32
|
+
expected_output_tokens=int(request.get("expected_output_tokens", 16)),
|
|
33
|
+
difficulty=float(request.get("difficulty", 0.5)),
|
|
34
|
+
cache_pressure=float(request.get("cache_pressure", 0)),
|
|
35
|
+
queue_pressure=float(request.get("queue_pressure", 0)),
|
|
36
|
+
)
|
|
37
|
+
print(json.dumps(asdict(ElasticInferencePlanner().plan(contract, profile, SystemSignals(
|
|
38
|
+
queue_pressure=profile.queue_pressure, cache_pressure=profile.cache_pressure,
|
|
39
|
+
))), indent=2))
|
|
40
|
+
return 0
|
|
41
|
+
config = load_config(args.config)
|
|
42
|
+
if args.command == "health":
|
|
43
|
+
results = {name: provider.health().__dict__ if hasattr(provider, "health") else {"healthy": True, "detail": "not configured"} for name, provider in config.providers.items()}
|
|
44
|
+
print(json.dumps(results, indent=2))
|
|
45
|
+
return 0 if all(result["healthy"] for result in results.values()) else 1
|
|
46
|
+
service = build_service(config)
|
|
47
|
+
from .gateway import create_server
|
|
48
|
+
print(f"LatencyOps gateway listening on http://{config.host}:{config.port}")
|
|
49
|
+
create_server(config.host, config.port, service).serve_forever()
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
raise SystemExit(main())
|
latencyops/comparison.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Benchmark comparison and constraint-satisfaction helpers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from .benchmark import BenchmarkRunner
|
|
7
|
+
from .metrics import summarize
|
|
8
|
+
from .models import InferencePlan, LatencyContract, LatencySample, RequestProfile
|
|
9
|
+
from .router import StreamingProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class WorkloadCase:
|
|
14
|
+
"""Synthetic workload metadata; prompt content is never included in reports."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
prompt: str
|
|
18
|
+
contract: LatencyContract
|
|
19
|
+
profile: RequestProfile
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_workload_matrix(
|
|
23
|
+
provider: StreamingProvider,
|
|
24
|
+
cases: Iterable[WorkloadCase],
|
|
25
|
+
plan: InferencePlan,
|
|
26
|
+
runner: BenchmarkRunner | None = None,
|
|
27
|
+
) -> list[dict[str, object]]:
|
|
28
|
+
"""Run cases and report measurements plus contract satisfaction."""
|
|
29
|
+
runner = runner or BenchmarkRunner()
|
|
30
|
+
rows = []
|
|
31
|
+
for case in cases:
|
|
32
|
+
result = runner.run_streaming(provider, case.prompt, plan)
|
|
33
|
+
sample = result.sample
|
|
34
|
+
rows.append({
|
|
35
|
+
"case": case.name,
|
|
36
|
+
"success": sample.success,
|
|
37
|
+
"ttft_ms": sample.ttft_ms,
|
|
38
|
+
"tpot_ms": sample.tpot_ms,
|
|
39
|
+
"end_to_end_ms": sample.end_to_end_ms,
|
|
40
|
+
"deadline_satisfied": sample.success and sample.end_to_end_ms <= case.contract.deadline_ms,
|
|
41
|
+
"quality_floor": case.contract.quality_floor,
|
|
42
|
+
})
|
|
43
|
+
return rows
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compare(samples_by_provider: dict[str, list[LatencySample]]) -> list[dict[str, object]]:
|
|
47
|
+
"""Return stable, ranked summaries without retaining prompts or responses."""
|
|
48
|
+
rows = []
|
|
49
|
+
for provider, samples in samples_by_provider.items():
|
|
50
|
+
summary = summarize(samples)
|
|
51
|
+
rows.append({"provider": provider, **summary})
|
|
52
|
+
return sorted(rows, key=lambda row: (row.get("e2e_p95_ms", float("inf")), row["provider"]))
|
latencyops/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""TOML configuration for the installable LatencyOps application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .adapters import OpenAIChatCompatibleProvider
|
|
9
|
+
from .gateway import GatewayService
|
|
10
|
+
from .models import ProviderCandidate
|
|
11
|
+
from .policy import ElasticInferencePlanner
|
|
12
|
+
from .router import ModelRouter
|
|
13
|
+
from .telemetry import PrometheusExporter, TelemetryRecorder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AppConfig:
|
|
18
|
+
"""Validated runtime configuration loaded from TOML and environment variables."""
|
|
19
|
+
|
|
20
|
+
host: str
|
|
21
|
+
port: int
|
|
22
|
+
providers: dict[str, OpenAIChatCompatibleProvider]
|
|
23
|
+
candidates: list[ProviderCandidate]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config(path: str | Path) -> AppConfig:
|
|
27
|
+
with Path(path).open("rb") as handle:
|
|
28
|
+
raw = tomllib.load(handle)
|
|
29
|
+
server = raw.get("server", {})
|
|
30
|
+
provider_table = raw.get("providers", {})
|
|
31
|
+
providers: dict[str, OpenAIChatCompatibleProvider] = {}
|
|
32
|
+
candidates: list[ProviderCandidate] = []
|
|
33
|
+
for name, settings in provider_table.items():
|
|
34
|
+
endpoint = settings.get("endpoint")
|
|
35
|
+
key_env = settings.get("api_key_env")
|
|
36
|
+
if not endpoint or not key_env:
|
|
37
|
+
raise ValueError(f"provider {name!r} requires endpoint and api_key_env")
|
|
38
|
+
key = os.environ.get(key_env)
|
|
39
|
+
if not key:
|
|
40
|
+
raise ValueError(f"environment variable {key_env!r} is not set")
|
|
41
|
+
models = settings.get("models", {})
|
|
42
|
+
provider = OpenAIChatCompatibleProvider(
|
|
43
|
+
name,
|
|
44
|
+
endpoint,
|
|
45
|
+
api_key=key,
|
|
46
|
+
timeout=float(settings.get("timeout_seconds", 30)),
|
|
47
|
+
model_names=models,
|
|
48
|
+
chat_template_kwargs=settings.get("chat_template_kwargs"),
|
|
49
|
+
health_endpoint=settings.get("health_endpoint"),
|
|
50
|
+
)
|
|
51
|
+
providers[name] = provider
|
|
52
|
+
for tier, model in models.items():
|
|
53
|
+
candidates.append(ProviderCandidate(name, tier, True, cost_per_1k_tokens=settings.get("cost_per_1k_tokens")))
|
|
54
|
+
missing = {"small", "standard", "large"} - {candidate.model_tier for candidate in candidates}
|
|
55
|
+
if missing:
|
|
56
|
+
raise ValueError(f"configuration is missing model tiers: {sorted(missing)}")
|
|
57
|
+
return AppConfig(str(server.get("host", "127.0.0.1")), int(server.get("port", 8080)), providers, candidates)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_service(config: AppConfig) -> GatewayService:
|
|
61
|
+
exporter = PrometheusExporter()
|
|
62
|
+
telemetry = TelemetryRecorder([exporter])
|
|
63
|
+
lookup = {candidate.name: config.providers[candidate.name] for candidate in config.candidates}
|
|
64
|
+
by_tier = {
|
|
65
|
+
candidate.model_tier: config.providers[candidate.name]
|
|
66
|
+
for candidate in config.candidates
|
|
67
|
+
}
|
|
68
|
+
return GatewayService(
|
|
69
|
+
ModelRouter(by_tier), ElasticInferencePlanner(), telemetry,
|
|
70
|
+
provider_candidates=config.candidates, provider_lookup=lookup,
|
|
71
|
+
)
|