throttle-pro 0.3.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.
- throttle/__init__.py +3 -0
- throttle/advisor.py +283 -0
- throttle/benchmark.py +2428 -0
- throttle/bottleneck_analysis.py +1115 -0
- throttle/cache.py +381 -0
- throttle/cli.py +3754 -0
- throttle/compare.py +1632 -0
- throttle/config.py +114 -0
- throttle/cost_model.py +83 -0
- throttle/diagnose.py +369 -0
- throttle/embeddings.py +160 -0
- throttle/experimental_tuning.py +1724 -0
- throttle/golden.py +1408 -0
- throttle/guidellm_backend.py +2193 -0
- throttle/models.py +349 -0
- throttle/prompts.jsonl +8 -0
- throttle/provenance.py +402 -0
- throttle/proxy.py +548 -0
- throttle/safety_validation.py +1544 -0
- throttle/server_metrics.py +1407 -0
- throttle/simulator.py +325 -0
- throttle/statistics.py +238 -0
- throttle/warmup_prompts.jsonl +3 -0
- throttle/workload.py +129 -0
- throttle_pro-0.3.0.dist-info/METADATA +890 -0
- throttle_pro-0.3.0.dist-info/RECORD +30 -0
- throttle_pro-0.3.0.dist-info/WHEEL +5 -0
- throttle_pro-0.3.0.dist-info/entry_points.txt +2 -0
- throttle_pro-0.3.0.dist-info/licenses/LICENSE +21 -0
- throttle_pro-0.3.0.dist-info/top_level.txt +1 -0
throttle/__init__.py
ADDED
throttle/advisor.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
throttle/advisor.py
|
|
3
|
+
===================
|
|
4
|
+
vLLM metric-to-dollar translation layer. Tier 1 only.
|
|
5
|
+
|
|
6
|
+
Reads vLLM /metrics (Prometheus text format), computes cost figures,
|
|
7
|
+
and streams one self-contained JSON snapshot per scrape interval.
|
|
8
|
+
|
|
9
|
+
Design constraints (from spec):
|
|
10
|
+
- Never estimates cost for unobserved configs
|
|
11
|
+
- Refuses to print $/Mtok when gen_throughput is unavailable
|
|
12
|
+
- Stops at Tier 1 — no recommendations, no window logic yet
|
|
13
|
+
- Small and readable: metrics arithmetic only
|
|
14
|
+
|
|
15
|
+
Consumed by a live view (not built here). Caller iterates stream_metrics()
|
|
16
|
+
and renders each snapshot however it wants.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import time
|
|
23
|
+
import urllib.request
|
|
24
|
+
import urllib.error
|
|
25
|
+
from dataclasses import dataclass, field, asdict
|
|
26
|
+
from typing import Iterator, Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Prometheus scraper — no dependencies
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def _scrape(url: str, timeout: float = 5.0) -> dict[str, float]:
|
|
34
|
+
"""
|
|
35
|
+
Scrape Prometheus text format from url.
|
|
36
|
+
Returns {metric_name: value} for gauge and counter lines.
|
|
37
|
+
Ignores histograms and summaries (not needed for Tier 1).
|
|
38
|
+
Raises on connection failure — caller decides how to handle.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
42
|
+
body = resp.read().decode("utf-8")
|
|
43
|
+
except urllib.error.URLError as e:
|
|
44
|
+
raise ConnectionError(f"Cannot reach {url}: {e}") from e
|
|
45
|
+
|
|
46
|
+
metrics: dict[str, float] = {}
|
|
47
|
+
for line in body.splitlines():
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if not line or line.startswith("#"):
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
# metric_name{labels} value [timestamp]
|
|
53
|
+
# or metric_name value [timestamp]
|
|
54
|
+
parts = line.rsplit(" ", 2)
|
|
55
|
+
value = float(parts[-2] if len(parts) == 3 else parts[-1])
|
|
56
|
+
name_part = parts[0]
|
|
57
|
+
# strip labels
|
|
58
|
+
name = name_part.split("{")[0]
|
|
59
|
+
# keep last value if duplicated (e.g. per-model labels)
|
|
60
|
+
metrics[name] = value
|
|
61
|
+
except (ValueError, IndexError):
|
|
62
|
+
continue
|
|
63
|
+
return metrics
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Tier 1 cost computation
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CostSnapshot:
|
|
72
|
+
# Metadata
|
|
73
|
+
timestamp: float
|
|
74
|
+
scrape_url: str
|
|
75
|
+
gpu_hourly_rate: float
|
|
76
|
+
observation_seconds: float # how long this tool has been running
|
|
77
|
+
|
|
78
|
+
# Raw metrics (None = not exposed by endpoint)
|
|
79
|
+
generation_throughput_toks_per_sec: Optional[float]
|
|
80
|
+
prompt_throughput_toks_per_sec: Optional[float]
|
|
81
|
+
num_requests_running: Optional[float]
|
|
82
|
+
num_requests_waiting: Optional[float]
|
|
83
|
+
gpu_cache_usage_perc: Optional[float]
|
|
84
|
+
num_preemptions_total: Optional[float]
|
|
85
|
+
|
|
86
|
+
# Derived cost (None = refused — see basis field)
|
|
87
|
+
cost_per_hour: Optional[float]
|
|
88
|
+
cost_per_million_tokens: Optional[float]
|
|
89
|
+
idle_fraction: Optional[float] # None until window logic added
|
|
90
|
+
|
|
91
|
+
# Batch fill (None if max_num_seqs unknown)
|
|
92
|
+
batch_fill: Optional[float]
|
|
93
|
+
max_num_seqs: Optional[int]
|
|
94
|
+
|
|
95
|
+
# Data quality
|
|
96
|
+
metrics_present: list[str] = field(default_factory=list)
|
|
97
|
+
metrics_unavailable: list[str] = field(default_factory=list)
|
|
98
|
+
refusals: list[dict] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return asdict(self)
|
|
102
|
+
|
|
103
|
+
def to_json(self) -> str:
|
|
104
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_TIER1_METRICS = [
|
|
108
|
+
"vllm:avg_generation_throughput_toks_per_s",
|
|
109
|
+
"vllm:avg_prompt_throughput_toks_per_s",
|
|
110
|
+
"vllm:num_requests_running",
|
|
111
|
+
"vllm:num_requests_waiting",
|
|
112
|
+
"vllm:gpu_cache_usage_perc",
|
|
113
|
+
"vllm:num_preemptions_total",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _build_snapshot(
|
|
118
|
+
raw: dict[str, float],
|
|
119
|
+
gpu_hourly_rate: float,
|
|
120
|
+
scrape_url: str,
|
|
121
|
+
observation_seconds: float,
|
|
122
|
+
max_num_seqs: Optional[int],
|
|
123
|
+
) -> CostSnapshot:
|
|
124
|
+
"""
|
|
125
|
+
Translate raw Prometheus metrics into a CostSnapshot.
|
|
126
|
+
Refuses cost computation when gen_throughput is unavailable.
|
|
127
|
+
"""
|
|
128
|
+
present = []
|
|
129
|
+
unavailable = []
|
|
130
|
+
refusals = []
|
|
131
|
+
|
|
132
|
+
def get(name: str) -> Optional[float]:
|
|
133
|
+
if name in raw:
|
|
134
|
+
present.append(name)
|
|
135
|
+
return raw[name]
|
|
136
|
+
else:
|
|
137
|
+
unavailable.append(name)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
gen_tput = get("vllm:avg_generation_throughput_toks_per_s")
|
|
141
|
+
prompt_tput = get("vllm:avg_prompt_throughput_toks_per_s")
|
|
142
|
+
running = get("vllm:num_requests_running")
|
|
143
|
+
waiting = get("vllm:num_requests_waiting")
|
|
144
|
+
kv_usage = get("vllm:gpu_cache_usage_perc")
|
|
145
|
+
preemptions = get("vllm:num_preemptions_total")
|
|
146
|
+
|
|
147
|
+
# Cost computation
|
|
148
|
+
gpu_rate_per_sec = gpu_hourly_rate / 3600.0
|
|
149
|
+
|
|
150
|
+
if gen_tput is None or gen_tput <= 0:
|
|
151
|
+
cost_per_hour = None
|
|
152
|
+
cost_per_mtok = None
|
|
153
|
+
if gen_tput is None:
|
|
154
|
+
refusals.append({
|
|
155
|
+
"figure": "cost_per_million_tokens",
|
|
156
|
+
"reason": (
|
|
157
|
+
"vllm:avg_generation_throughput_toks_per_s not exposed. "
|
|
158
|
+
"Cannot compute cost without throughput denominator."
|
|
159
|
+
),
|
|
160
|
+
})
|
|
161
|
+
else:
|
|
162
|
+
refusals.append({
|
|
163
|
+
"figure": "cost_per_million_tokens",
|
|
164
|
+
"reason": (
|
|
165
|
+
"Generation throughput is zero. "
|
|
166
|
+
"Is the backend serving requests?"
|
|
167
|
+
),
|
|
168
|
+
})
|
|
169
|
+
else:
|
|
170
|
+
cost_per_hour = gpu_hourly_rate
|
|
171
|
+
cost_per_mtok = round((gpu_rate_per_sec / gen_tput) * 1_000_000, 4)
|
|
172
|
+
|
|
173
|
+
# Batch fill
|
|
174
|
+
batch_fill = None
|
|
175
|
+
if running is not None and max_num_seqs is not None and max_num_seqs > 0:
|
|
176
|
+
batch_fill = round(running / max_num_seqs, 3)
|
|
177
|
+
|
|
178
|
+
return CostSnapshot(
|
|
179
|
+
timestamp=time.time(),
|
|
180
|
+
scrape_url=scrape_url,
|
|
181
|
+
gpu_hourly_rate=gpu_hourly_rate,
|
|
182
|
+
observation_seconds=round(observation_seconds, 1),
|
|
183
|
+
generation_throughput_toks_per_sec=gen_tput,
|
|
184
|
+
prompt_throughput_toks_per_sec=prompt_tput,
|
|
185
|
+
num_requests_running=running,
|
|
186
|
+
num_requests_waiting=waiting,
|
|
187
|
+
gpu_cache_usage_perc=kv_usage,
|
|
188
|
+
num_preemptions_total=preemptions,
|
|
189
|
+
cost_per_hour=cost_per_hour,
|
|
190
|
+
cost_per_million_tokens=cost_per_mtok,
|
|
191
|
+
idle_fraction=None, # Tier 2 — window logic not yet built
|
|
192
|
+
batch_fill=batch_fill,
|
|
193
|
+
max_num_seqs=max_num_seqs,
|
|
194
|
+
metrics_present=present,
|
|
195
|
+
metrics_unavailable=unavailable,
|
|
196
|
+
refusals=refusals,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# Public streaming interface
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def stream_metrics(
|
|
205
|
+
metrics_url: str,
|
|
206
|
+
gpu_rate_per_hour: float,
|
|
207
|
+
interval_seconds: float = 15.0,
|
|
208
|
+
max_num_seqs: Optional[int] = None,
|
|
209
|
+
) -> Iterator[CostSnapshot]:
|
|
210
|
+
"""
|
|
211
|
+
Yields one CostSnapshot per scrape interval. Never returns.
|
|
212
|
+
Each snapshot is self-contained — caller renders or stores it.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
metrics_url: vLLM /metrics endpoint, e.g. http://localhost:8000/metrics
|
|
216
|
+
gpu_rate_per_hour: GPU cost in $/hr. Required — no default.
|
|
217
|
+
interval_seconds: scrape interval (default 15s)
|
|
218
|
+
max_num_seqs: vLLM max_num_seqs for batch fill computation.
|
|
219
|
+
If None, batch_fill is omitted from snapshots.
|
|
220
|
+
"""
|
|
221
|
+
start = time.time()
|
|
222
|
+
|
|
223
|
+
while True:
|
|
224
|
+
t0 = time.perf_counter()
|
|
225
|
+
observation_seconds = time.time() - start
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
raw = _scrape(metrics_url)
|
|
229
|
+
except ConnectionError as e:
|
|
230
|
+
# Yield an error snapshot rather than crashing the stream
|
|
231
|
+
yield CostSnapshot(
|
|
232
|
+
timestamp=time.time(),
|
|
233
|
+
scrape_url=metrics_url,
|
|
234
|
+
gpu_hourly_rate=gpu_rate_per_hour,
|
|
235
|
+
observation_seconds=round(observation_seconds, 1),
|
|
236
|
+
generation_throughput_toks_per_sec=None,
|
|
237
|
+
prompt_throughput_toks_per_sec=None,
|
|
238
|
+
num_requests_running=None,
|
|
239
|
+
num_requests_waiting=None,
|
|
240
|
+
gpu_cache_usage_perc=None,
|
|
241
|
+
num_preemptions_total=None,
|
|
242
|
+
cost_per_hour=None,
|
|
243
|
+
cost_per_million_tokens=None,
|
|
244
|
+
idle_fraction=None,
|
|
245
|
+
batch_fill=None,
|
|
246
|
+
max_num_seqs=max_num_seqs,
|
|
247
|
+
metrics_present=[],
|
|
248
|
+
metrics_unavailable=_TIER1_METRICS,
|
|
249
|
+
refusals=[{"figure": "all", "reason": str(e)}],
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
yield _build_snapshot(
|
|
253
|
+
raw=raw,
|
|
254
|
+
gpu_hourly_rate=gpu_rate_per_hour,
|
|
255
|
+
scrape_url=metrics_url,
|
|
256
|
+
observation_seconds=observation_seconds,
|
|
257
|
+
max_num_seqs=max_num_seqs,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Sleep for remainder of interval
|
|
261
|
+
elapsed = time.perf_counter() - t0
|
|
262
|
+
sleep = max(0.0, interval_seconds - elapsed)
|
|
263
|
+
time.sleep(sleep)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# One-shot snapshot (for --try and testing)
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def snapshot_once(
|
|
271
|
+
metrics_url: str,
|
|
272
|
+
gpu_rate_per_hour: float,
|
|
273
|
+
max_num_seqs: Optional[int] = None,
|
|
274
|
+
) -> CostSnapshot:
|
|
275
|
+
"""Single scrape — used by --try and tests. Does not loop."""
|
|
276
|
+
raw = _scrape(metrics_url)
|
|
277
|
+
return _build_snapshot(
|
|
278
|
+
raw=raw,
|
|
279
|
+
gpu_hourly_rate=gpu_rate_per_hour,
|
|
280
|
+
scrape_url=metrics_url,
|
|
281
|
+
observation_seconds=0.0,
|
|
282
|
+
max_num_seqs=max_num_seqs,
|
|
283
|
+
)
|