workingset 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.
- workingset/__init__.py +6 -0
- workingset/cli.py +412 -0
- workingset/config.py +411 -0
- workingset/hypotheses/__init__.py +152 -0
- workingset/hypotheses/base.py +230 -0
- workingset/hypotheses/burst.py +85 -0
- workingset/hypotheses/ceilings.py +226 -0
- workingset/hypotheses/context.py +339 -0
- workingset/hypotheses/gaps.py +646 -0
- workingset/metrics/__init__.py +59 -0
- workingset/metrics/adapter.py +340 -0
- workingset/metrics/cli.py +310 -0
- workingset/metrics/parse.py +384 -0
- workingset/metrics/sampler.py +1141 -0
- workingset/metrics/vllm.py +311 -0
- workingset/model.py +3720 -0
- workingset/predict.py +225 -0
- workingset/probe/__init__.py +49 -0
- workingset/probe/burst.py +168 -0
- workingset/probe/ladder.py +30 -0
- workingset/probe/options.py +45 -0
- workingset/probe/population.py +511 -0
- workingset/probe/request.py +470 -0
- workingset/probe/session.py +186 -0
- workingset/probe/stats.py +58 -0
- workingset/record.py +290 -0
- workingset/report.py +358 -0
- workingset/shared.py +2163 -0
- workingset/test_cmd.py +491 -0
- workingset/workload.py +2061 -0
- workingset-0.1.0.dist-info/METADATA +118 -0
- workingset-0.1.0.dist-info/RECORD +35 -0
- workingset-0.1.0.dist-info/WHEEL +4 -0
- workingset-0.1.0.dist-info/entry_points.txt +2 -0
- workingset-0.1.0.dist-info/licenses/LICENSE +21 -0
workingset/__init__.py
ADDED
workingset/cli.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""`ws` — the workingset command line.
|
|
2
|
+
|
|
3
|
+
ws predict CONFIG [--closed] [--json] price a configuration
|
|
4
|
+
ws test CONFIG [H-key ...] [--exclusive] test predictions on an endpoint
|
|
5
|
+
ws report RUN.json re-print a run's report
|
|
6
|
+
ws hypotheses list the H-* and what they need
|
|
7
|
+
ws init [--model KEY --gpu PART --tp N ...] write a starter config
|
|
8
|
+
ws models list model / GPU keys
|
|
9
|
+
ws selfcheck run the model's self-checks
|
|
10
|
+
ws metrics probe|tail|window ... sample a live /metrics endpoint
|
|
11
|
+
ws workload --prometheus URL --range 7d measure the real workload and
|
|
12
|
+
emit the config's [workload]
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import replace
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from . import model as M
|
|
24
|
+
from .config import RunConfig, load_config
|
|
25
|
+
from .predict import predict
|
|
26
|
+
from .test_cmd import cmd_hypotheses, cmd_report, cmd_test
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fmt(x) -> str:
|
|
30
|
+
if isinstance(x, float) and not math.isfinite(x):
|
|
31
|
+
return "inf"
|
|
32
|
+
if isinstance(x, int) and x >= 999999:
|
|
33
|
+
return "never binds"
|
|
34
|
+
if isinstance(x, float):
|
|
35
|
+
return f"{x:,.2f}"
|
|
36
|
+
if isinstance(x, int):
|
|
37
|
+
return f"{x:,}"
|
|
38
|
+
return str(x)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fmt_count(x: float) -> str:
|
|
42
|
+
"""A population count without decimal noise when it is integral."""
|
|
43
|
+
return f"{int(x):,}" if float(x).is_integer() else f"{x:,.2f}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_predict(args) -> int:
|
|
47
|
+
cfg = load_config(args.config)
|
|
48
|
+
cfg = _apply_overrides(cfg, args)
|
|
49
|
+
p = predict(cfg, closed=args.closed, n_iter=args.n_iter, seed=args.seed)
|
|
50
|
+
if args.json:
|
|
51
|
+
out = {"config": cfg.to_dict(), "predictions": p.to_dict(),
|
|
52
|
+
"system": p.system(), "mode": "closed" if args.closed else "open",
|
|
53
|
+
"workingset": __version__}
|
|
54
|
+
print(json.dumps(out, indent=2, allow_nan=False))
|
|
55
|
+
return 0
|
|
56
|
+
d, w = cfg.deployment, cfg.workload
|
|
57
|
+
print(f"{cfg.to_model().name} on {d.gpus} (TP{d.tensor_parallel} x DP{d.replicas}), "
|
|
58
|
+
f"chunk {d.max_num_batched_tokens:,}, max_model_len {d.max_model_len:,}")
|
|
59
|
+
users = cfg.users_per_group()
|
|
60
|
+
print(f"operating point: {users:g} users/group, think {w.think_time_s} s, "
|
|
61
|
+
f"miss {w.miss_rate:.0%}, {'closed' if args.closed else 'open'} loop")
|
|
62
|
+
if w.headcount is not None:
|
|
63
|
+
sessions = M.sessions_from_headcount(
|
|
64
|
+
w.headcount, w.peak_active_share, w.sessions_per_active_user)
|
|
65
|
+
print(f"population: {w.headcount:,} people x {w.peak_active_share:g} active x "
|
|
66
|
+
f"{w.sessions_per_active_user:.1f} sessions = {_fmt_count(sessions)} sessions "
|
|
67
|
+
f"-> {_fmt_count(users)} /group on DP{d.replicas}")
|
|
68
|
+
print()
|
|
69
|
+
rows = [("cache (warm p5, users)", p.warm_capacity_p5),
|
|
70
|
+
("decode (users at floor)", p.decode_ceiling_users),
|
|
71
|
+
("latency (miss TTFT = budget)", p.latency_ceiling_users),
|
|
72
|
+
("saturation (prefill duty 100%)", p.saturation_ceiling_users)]
|
|
73
|
+
for k, v in rows:
|
|
74
|
+
mark = " <- binds" if k.startswith(p.binding_constraint) else ""
|
|
75
|
+
people = ""
|
|
76
|
+
if w.headcount is not None:
|
|
77
|
+
denom = w.peak_active_share * w.sessions_per_active_user
|
|
78
|
+
people = f" ≈ {_fmt_count(v * d.replicas / denom)} people"
|
|
79
|
+
print(f" {k:32} {_fmt(v):>14}{people}{mark}")
|
|
80
|
+
if p.replicas > 1:
|
|
81
|
+
print(f" (per replica group; x{p.replicas} under balanced routing)")
|
|
82
|
+
print()
|
|
83
|
+
print(f" req/s main {p.req_rate_main} prefill duty {p.prefill_duty:.1%} "
|
|
84
|
+
f"TTFT miss {_fmt(p.ttft_miss_s)} s hit {_fmt(p.ttft_hit_s)} s "
|
|
85
|
+
f"B* {p.bstar_misses}")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _apply_overrides(cfg: RunConfig, args) -> RunConfig:
|
|
90
|
+
dep = {k: v for k, v in (("model", args.model), ("gpu", args.gpu),
|
|
91
|
+
("tensor_parallel", args.tp), ("replicas", args.dp),
|
|
92
|
+
("weight_dtype", args.weight_dtype),
|
|
93
|
+
("kv_dtype", args.kv_dtype),
|
|
94
|
+
("max_num_batched_tokens", args.chunk),
|
|
95
|
+
("max_model_len", args.max_model_len),
|
|
96
|
+
("ram_gib", args.ram_gib)) if v is not None}
|
|
97
|
+
wl = {k: v for k, v in (("users", args.users), ("miss_rate", args.miss_rate),
|
|
98
|
+
("think_time_s", args.think)) if v is not None}
|
|
99
|
+
if dep:
|
|
100
|
+
cfg = replace(cfg, deployment=replace(cfg.deployment, **dep))
|
|
101
|
+
if wl:
|
|
102
|
+
if "users" in wl:
|
|
103
|
+
wl["headcount"] = None
|
|
104
|
+
cfg = replace(cfg, workload=replace(cfg.workload, **wl))
|
|
105
|
+
return cfg
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_init(args) -> int:
|
|
109
|
+
cfg = _apply_overrides(RunConfig(), args)
|
|
110
|
+
cfg.validate()
|
|
111
|
+
text = cfg.dumps("json" if args.json else "toml")
|
|
112
|
+
# the default name follows the format: `ws init --json` writing JSON into
|
|
113
|
+
# workingset.toml left the next `ws predict` failing on a TOML parse
|
|
114
|
+
out = args.output if args.output is not None else (
|
|
115
|
+
"workingset.json" if args.json else "workingset.toml")
|
|
116
|
+
if out and out != "-":
|
|
117
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
118
|
+
f.write(text)
|
|
119
|
+
print(f"wrote {out}")
|
|
120
|
+
else:
|
|
121
|
+
sys.stdout.write(text)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_models(_args) -> int:
|
|
126
|
+
print("models:")
|
|
127
|
+
for k, m in M.MODELS.items():
|
|
128
|
+
arms = "fp8" + (", nvfp4" if m.nvfp4_w else "")
|
|
129
|
+
print(f" {k:8} {m.name} [{arms}]")
|
|
130
|
+
print("gpus:")
|
|
131
|
+
for k, g in M.GPUS.items():
|
|
132
|
+
print(f" {k:8} {g.vram / 1e9:.0f} GB, {g.hbm_bw / 1e12:.1f} TB/s"
|
|
133
|
+
+ (", nvfp4" if g.supports_nvfp4 else ""))
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_selfcheck(_args) -> int:
|
|
138
|
+
M._selfcheck()
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _dflt(obj, field: str, fmt: str = "") -> str:
|
|
143
|
+
""""(default X)" rendered from the dataclass field itself.
|
|
144
|
+
|
|
145
|
+
One source of truth: a help string that carries its own copy of a default
|
|
146
|
+
drifts the first time the default moves, and two of these already had —
|
|
147
|
+
`--max-probe-tokens` said 2,000,000 against a code default of 4,000,000,
|
|
148
|
+
and `--shared-rounds` said 3 against 4.
|
|
149
|
+
"""
|
|
150
|
+
v = getattr(obj, field)
|
|
151
|
+
if v is None:
|
|
152
|
+
return "default: off"
|
|
153
|
+
return f"default {v:{fmt}}" if fmt else f"default {v}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _add_deploy_flags(ap: argparse.ArgumentParser) -> None:
|
|
157
|
+
ap.add_argument("--model", help="model key (see `ws models`)")
|
|
158
|
+
ap.add_argument("--gpu", help="GPU part (H200, B300)")
|
|
159
|
+
ap.add_argument("--tp", type=int, help="tensor-parallel group size")
|
|
160
|
+
ap.add_argument("--dp", type=int, help="data-parallel replica groups")
|
|
161
|
+
ap.add_argument("--weight-dtype", choices=M.WEIGHT_DTYPES)
|
|
162
|
+
ap.add_argument("--kv-dtype", choices=M.KV_DTYPES)
|
|
163
|
+
ap.add_argument("--chunk", type=int, help="max_num_batched_tokens")
|
|
164
|
+
ap.add_argument("--max-model-len", type=int)
|
|
165
|
+
ap.add_argument("--ram-gib", type=float, help="CPU KV offload per group, GiB")
|
|
166
|
+
ap.add_argument("--users", type=float,
|
|
167
|
+
help="operating point, users per group (fractional is "
|
|
168
|
+
"meaningful: it is a load, not a population)")
|
|
169
|
+
ap.add_argument("--miss-rate", type=float)
|
|
170
|
+
ap.add_argument("--think", type=float, help="think time, s")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
174
|
+
ap = argparse.ArgumentParser(prog="ws", description=__doc__,
|
|
175
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
176
|
+
ap.add_argument("--version", action="version", version=f"workingset {__version__}")
|
|
177
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
178
|
+
|
|
179
|
+
p = sub.add_parser("predict", help="price a configuration")
|
|
180
|
+
p.add_argument("config", help="workingset.toml / .json / a harness .py")
|
|
181
|
+
p.add_argument("--closed", action="store_true",
|
|
182
|
+
help="closed-loop conversion (think_time_s is then the WAITING "
|
|
183
|
+
"time Z per request; the model supplies the service time)")
|
|
184
|
+
p.add_argument("--json", action="store_true")
|
|
185
|
+
p.add_argument("--n-iter", type=int, default=400)
|
|
186
|
+
p.add_argument("--seed", type=int, default=0)
|
|
187
|
+
_add_deploy_flags(p)
|
|
188
|
+
p.set_defaults(fn=cmd_predict)
|
|
189
|
+
|
|
190
|
+
p = sub.add_parser("init", help="write a starter config")
|
|
191
|
+
p.add_argument("-o", "--output", default=None,
|
|
192
|
+
help="output path ('-' for stdout); default "
|
|
193
|
+
"workingset.toml, or workingset.json with --json")
|
|
194
|
+
p.add_argument("--json", action="store_true")
|
|
195
|
+
_add_deploy_flags(p)
|
|
196
|
+
p.set_defaults(fn=cmd_init)
|
|
197
|
+
|
|
198
|
+
p = sub.add_parser(
|
|
199
|
+
"test", help="test the predictions against a live endpoint",
|
|
200
|
+
description="Put a configuration's predictions to a live "
|
|
201
|
+
"OpenAI-compatible endpoint, one hypothesis at a time.",
|
|
202
|
+
epilog="Examples:\n"
|
|
203
|
+
" ws test workingset.toml --dry-run\n"
|
|
204
|
+
" ws test workingset.toml --all --exclusive --out run.json\n"
|
|
205
|
+
" ws test workingset.toml H-ttft-miss H-itl-mean\n"
|
|
206
|
+
" ws test workingset.toml --exclusive --burst 8\n",
|
|
207
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
208
|
+
p.add_argument("config", help="workingset.toml / .json / a harness .py")
|
|
209
|
+
p.add_argument("keys", nargs="*", metavar="H-key",
|
|
210
|
+
help="hypotheses to test (default: all; see `ws hypotheses`)")
|
|
211
|
+
p.add_argument("--all", action="store_true",
|
|
212
|
+
help="every hypothesis (the default when no key is given)")
|
|
213
|
+
p.add_argument("--exclusive", action="store_true",
|
|
214
|
+
help="this run OWNS the endpoint and may generate the "
|
|
215
|
+
"population it measures. Without it, hypotheses that "
|
|
216
|
+
"need to are listed as skipped, never silently run.")
|
|
217
|
+
p.add_argument("--dry-run", action="store_true",
|
|
218
|
+
help="print the plan, predictions, selected hypotheses and "
|
|
219
|
+
"the sampler self-check; send nothing")
|
|
220
|
+
# endpoint overrides. `--model` stays the model KEY here, as it is in
|
|
221
|
+
# `ws predict` and `ws init` — a downloaded harness names only the served
|
|
222
|
+
# checkpoint, so `ws test harness.py --model 27B` is how you say which
|
|
223
|
+
# model it is. The served id (what goes in the JSON body) is --model-id.
|
|
224
|
+
p.add_argument("--base-url", help="override the config's endpoint base URL")
|
|
225
|
+
p.add_argument("--model-id", "--served-model", dest="model_id",
|
|
226
|
+
help="override the served model id sent in the request body")
|
|
227
|
+
p.add_argument("--api-key-env", help="env var NAME holding the API key")
|
|
228
|
+
p.add_argument("--api", choices=("completions", "chat"),
|
|
229
|
+
help="which OpenAI-compatible route to drive "
|
|
230
|
+
"(default: completions, as the harness does)")
|
|
231
|
+
p.add_argument("--metrics-url", help="vLLM /metrics, for server-side "
|
|
232
|
+
"covariates and window deltas")
|
|
233
|
+
# probe knobs
|
|
234
|
+
p.add_argument("--rungs", help="ladder multipliers of predicted_limit_users")
|
|
235
|
+
p.add_argument("--max-users", type=int, help="hard cap on any rung")
|
|
236
|
+
p.add_argument("--ramp-s", type=float, help="per-rung ramp, s")
|
|
237
|
+
p.add_argument("--measure-s", type=float, help="per-rung measure window, s")
|
|
238
|
+
p.add_argument("--turns-per-user", type=int,
|
|
239
|
+
help="cap turns per session, 0 = unlimited in the window")
|
|
240
|
+
p.add_argument("--burst", type=int, metavar="N",
|
|
241
|
+
help="fire N simultaneous forced misses (enables H-burst)")
|
|
242
|
+
p.add_argument("--burst-users", type=int,
|
|
243
|
+
help="standing load for the burst (default: operating point)")
|
|
244
|
+
p.add_argument("--sample-requests", type=int,
|
|
245
|
+
help="sessions the cheap probe opens (default 8)")
|
|
246
|
+
p.add_argument("--chars-per-token", type=float,
|
|
247
|
+
help="synthetic-text calibration (default 4.0)")
|
|
248
|
+
p.add_argument("--context-cap-tokens", type=int,
|
|
249
|
+
help="cap on sampled contexts (default: max_model_len)")
|
|
250
|
+
p.add_argument("--request-timeout-s", type=float,
|
|
251
|
+
help="per-request read timeout; keep well above the TTFT "
|
|
252
|
+
"budget so breaches are measured, not dropped")
|
|
253
|
+
p.add_argument("--freeze-threshold-ms", type=float, metavar="MS",
|
|
254
|
+
help="a gap at or above this counts as a FREEZE "
|
|
255
|
+
"(default 100). Keep it BELOW the smaller predicted "
|
|
256
|
+
"freeze; the freeze ladder is threshold-free.")
|
|
257
|
+
# --- shared-endpoint safety rails ------------------------------------
|
|
258
|
+
# These bind WITHOUT --exclusive, where the endpoint belongs to somebody
|
|
259
|
+
# else; --exclusive takes them all off. Every "(default X)" below is
|
|
260
|
+
# rendered from the dataclass field itself by `_dflt`, because a help
|
|
261
|
+
# string carrying its own copy of a default is a help string that will one
|
|
262
|
+
# day be wrong — and this one already was, on two flags.
|
|
263
|
+
from .shared import ProbeBudget, SharedOptions
|
|
264
|
+
|
|
265
|
+
bud, sh = ProbeBudget(), SharedOptions()
|
|
266
|
+
|
|
267
|
+
def _b(field: str, fmt: str = "") -> str:
|
|
268
|
+
return _dflt(bud, field, fmt)
|
|
269
|
+
|
|
270
|
+
def _s(field: str, fmt: str = "") -> str:
|
|
271
|
+
return _dflt(sh, field, fmt)
|
|
272
|
+
|
|
273
|
+
g = p.add_argument_group(
|
|
274
|
+
"shared-endpoint safety rails",
|
|
275
|
+
"Bind without --exclusive. Any rail that trips aborts the run, "
|
|
276
|
+
"records the reason and exits non-zero.")
|
|
277
|
+
g.add_argument("--max-extra-load", type=int, metavar="N",
|
|
278
|
+
help="never more than N of OUR requests in flight, canary "
|
|
279
|
+
f"included ({_b('max_extra_load')}; 0 = no cap). "
|
|
280
|
+
"Enforced as given: never raised to make room for the "
|
|
281
|
+
"canary")
|
|
282
|
+
g.add_argument("--abort-if-waiting", type=float, metavar="N",
|
|
283
|
+
help="abort when the server's requests_waiting gauge "
|
|
284
|
+
f"exceeds N ({_b('abort_if_waiting', 'g')}, i.e. abort "
|
|
285
|
+
"on any queue). Needs --metrics-url")
|
|
286
|
+
g.add_argument("--abort-if-kv-above", type=float, metavar="F",
|
|
287
|
+
help="abort when KV occupancy exceeds this fraction "
|
|
288
|
+
f"({_b('abort_if_kv_above', 'g')}). Needs "
|
|
289
|
+
"--metrics-url")
|
|
290
|
+
g.add_argument("--max-gauge-age-s", type=float, metavar="S",
|
|
291
|
+
help="a gauge reading older than S seconds is treated as a "
|
|
292
|
+
"failed read (default: the sampler's own staleness "
|
|
293
|
+
"rule, 3 scrape intervals + one timeout). Without "
|
|
294
|
+
"this a dead sampler keeps handing back its last good "
|
|
295
|
+
"snapshot and the rails never notice")
|
|
296
|
+
g.add_argument("--max-metrics-gaps", type=int, metavar="N",
|
|
297
|
+
help="abort after N consecutive failed gauge reads once "
|
|
298
|
+
f"the sampler has been working ({_b('max_metrics_gaps')}"
|
|
299
|
+
"; 0 = off). Fails CLOSED: without it a sampler dying "
|
|
300
|
+
"mid-run disarms the queue and KV rails silently")
|
|
301
|
+
g.add_argument("--max-probe-tokens", type=int, metavar="T",
|
|
302
|
+
help="total intended prompt tokens the run may send "
|
|
303
|
+
f"({_b('max_probe_tokens', ',')}; 0 = no cap)")
|
|
304
|
+
g.add_argument("--no-canary", action="store_true",
|
|
305
|
+
help="drop the periodic 1-token canary (it is the only "
|
|
306
|
+
"contention signal when no --metrics-url is given)")
|
|
307
|
+
g.add_argument("--canary-every-s", type=float, metavar="S",
|
|
308
|
+
help=f"seconds between canary requests "
|
|
309
|
+
f"({_b('canary_every_s', 'g')})")
|
|
310
|
+
g.add_argument("--canary-baseline-s", type=float, metavar="S",
|
|
311
|
+
help="the run's first S seconds set the canary baseline "
|
|
312
|
+
f"p50 ({_b('canary_baseline_s', 'g')})")
|
|
313
|
+
g.add_argument("--canary-window-s", type=float, metavar="S",
|
|
314
|
+
help="trailing window the canary p50 is compared over "
|
|
315
|
+
f"({_b('canary_window_s', 'g')})")
|
|
316
|
+
g.add_argument("--canary-drift", type=float, metavar="X",
|
|
317
|
+
help="abort when the trailing canary p50 exceeds X times "
|
|
318
|
+
f"the baseline p50 ({_b('canary_drift', 'g')})")
|
|
319
|
+
g.add_argument("--canary-min-n", type=int, metavar="N",
|
|
320
|
+
help="samples each canary window needs before the drift "
|
|
321
|
+
f"rule can fire ({_b('canary_min_n')})")
|
|
322
|
+
# --- shared-endpoint covariate fit -----------------------------------
|
|
323
|
+
g = p.add_argument_group(
|
|
324
|
+
"shared-endpoint covariate fit",
|
|
325
|
+
"Other people's traffic is a covariate, not noise: the probe stamps "
|
|
326
|
+
"every request with the server's load and regresses it out.")
|
|
327
|
+
g.add_argument("--shared-lengths", metavar="F,F,...",
|
|
328
|
+
help="prompt lengths to probe, as fractions of the context "
|
|
329
|
+
f"cap ({_s('lengths')})")
|
|
330
|
+
g.add_argument("--shared-rounds", type=int, metavar="N",
|
|
331
|
+
help=f"passes over the prompt-length ladder ({_s('rounds')})")
|
|
332
|
+
g.add_argument("--shared-warm-turns", type=int, metavar="N",
|
|
333
|
+
help=f"warm prefix-hit turns per round ({_s('warm_turns')})")
|
|
334
|
+
g.add_argument("--shared-ladder", action="store_true",
|
|
335
|
+
help="run for --shared-duration-s cycling the lengths, and "
|
|
336
|
+
"report TTFT/ITL binned by the concurrency the server "
|
|
337
|
+
"happened to be carrying")
|
|
338
|
+
g.add_argument("--shared-duration-s", type=float, metavar="S",
|
|
339
|
+
help=f"length of a --shared-ladder run ({_s('duration_s', 'g')})")
|
|
340
|
+
g.add_argument("--max-extrapolation", type=float, metavar="SD",
|
|
341
|
+
help="a fitted verdict is refused when the operating point "
|
|
342
|
+
"lies more than SD observed standard deviations "
|
|
343
|
+
"outside the probed range of any regressor "
|
|
344
|
+
f"({_s('max_extrapolation', 'g')})")
|
|
345
|
+
g.add_argument("--max-extrapolation-requests", type=float, metavar="N",
|
|
346
|
+
help="the same gate in REQUESTS, so a noisy background "
|
|
347
|
+
"cannot buy a wider absolute licence than a quiet one "
|
|
348
|
+
f"({_s('max_extrapolation_requests', 'g')}). Both "
|
|
349
|
+
"gates bind")
|
|
350
|
+
g.add_argument("--verdict-sigmas", type=float, metavar="K",
|
|
351
|
+
help="a fitted verdict must not change within +/- K "
|
|
352
|
+
"standard errors of the fitted value, or the row stays "
|
|
353
|
+
f"not_established ({_s('verdict_sigmas', 'g')})")
|
|
354
|
+
g.add_argument("--min-local-n", type=int, metavar="N",
|
|
355
|
+
help="observations required within --local-radius of the "
|
|
356
|
+
"operating point in the load covariates "
|
|
357
|
+
f"({_s('min_local_n')}). Without this a design that "
|
|
358
|
+
"only ever saw two load levels can report the chord "
|
|
359
|
+
"of a curve it never sampled the middle of")
|
|
360
|
+
g.add_argument("--local-radius", type=float, metavar="SD",
|
|
361
|
+
help="radius of that neighbourhood, in standardised units "
|
|
362
|
+
f"({_s('local_radius', 'g')})")
|
|
363
|
+
g.add_argument("--engine", metavar="ID",
|
|
364
|
+
help="select ONE engine from a multi-engine /metrics dump. "
|
|
365
|
+
"Required for a fit when several are exported: "
|
|
366
|
+
"otherwise the request gauges are sums across engines "
|
|
367
|
+
"while the operating point is per replica group, and "
|
|
368
|
+
"KV occupancy cannot be combined at all")
|
|
369
|
+
p.add_argument("--seed", type=int, help="probe RNG seed")
|
|
370
|
+
p.add_argument("--no-ignore-eos", action="store_true",
|
|
371
|
+
help="drop the vLLM ignore_eos extension (strict OpenAI "
|
|
372
|
+
"endpoints); decode tok/s then depends on natural EOS")
|
|
373
|
+
p.add_argument("--n-iter", type=int, default=400,
|
|
374
|
+
help="model Monte-Carlo iterations for the predictions")
|
|
375
|
+
p.add_argument("--predict-seed", type=int, default=0)
|
|
376
|
+
p.add_argument("-o", "--out", help="write the run record here (JSON)")
|
|
377
|
+
_add_deploy_flags(p)
|
|
378
|
+
p.set_defaults(fn=cmd_test)
|
|
379
|
+
|
|
380
|
+
p = sub.add_parser("report", help="re-print a run record's report")
|
|
381
|
+
p.add_argument("record", help="run.json written by `ws test --out`")
|
|
382
|
+
p.set_defaults(fn=cmd_report)
|
|
383
|
+
|
|
384
|
+
p = sub.add_parser("hypotheses", help="list the H-* and what they need")
|
|
385
|
+
p.set_defaults(fn=cmd_hypotheses)
|
|
386
|
+
|
|
387
|
+
p = sub.add_parser("models", help="list model and GPU keys")
|
|
388
|
+
p.set_defaults(fn=cmd_models)
|
|
389
|
+
|
|
390
|
+
p = sub.add_parser("selfcheck", help="run the model's self-checks")
|
|
391
|
+
p.set_defaults(fn=cmd_selfcheck)
|
|
392
|
+
|
|
393
|
+
# `ws metrics` owns its own subtree, and builds it itself.
|
|
394
|
+
from .metrics.cli import add_subparser as _add_metrics
|
|
395
|
+
_add_metrics(sub)
|
|
396
|
+
from .workload import add_subparser as _add_workload
|
|
397
|
+
_add_workload(sub)
|
|
398
|
+
return ap
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def main(argv=None) -> int:
|
|
402
|
+
args = build_parser().parse_args(argv)
|
|
403
|
+
try:
|
|
404
|
+
return args.fn(args)
|
|
405
|
+
except (ValueError, KeyError, FileNotFoundError) as e:
|
|
406
|
+
# a config or model refusal is a user-facing message, not a traceback
|
|
407
|
+
print(f"ws: {e.args[0] if e.args else e}", file=sys.stderr)
|
|
408
|
+
return 2
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
if __name__ == "__main__":
|
|
412
|
+
sys.exit(main())
|