onpar 1.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.
- onpar/__init__.py +28 -0
- onpar/advise.py +525 -0
- onpar/atomicio.py +220 -0
- onpar/box.py +1241 -0
- onpar/brief.py +341 -0
- onpar/cache.py +636 -0
- onpar/catalog.py +339 -0
- onpar/catalog_update.py +625 -0
- onpar/cli.py +2463 -0
- onpar/core.py +167 -0
- onpar/desktop.py +248 -0
- onpar/distill/__init__.py +0 -0
- onpar/distill/cluster.py +271 -0
- onpar/distill/name.py +329 -0
- onpar/distill/shape.py +336 -0
- onpar/engine.py +119 -0
- onpar/engines.py +1197 -0
- onpar/events.py +213 -0
- onpar/fit.py +685 -0
- onpar/guard.py +436 -0
- onpar/hardware.py +406 -0
- onpar/hardware_catalog.py +250 -0
- onpar/host.py +1537 -0
- onpar/intent.py +1087 -0
- onpar/k8s/__init__.py +0 -0
- onpar/k8s/controller.py +403 -0
- onpar/k8s/nodes.py +523 -0
- onpar/k8s/reconcile.py +633 -0
- onpar/kernels.py +419 -0
- onpar/launch.py +1128 -0
- onpar/mcp.py +1303 -0
- onpar/measure.py +689 -0
- onpar/migrate.py +425 -0
- onpar/models.json +291 -0
- onpar/observe.py +355 -0
- onpar/plan.py +900 -0
- onpar/posttrain.py +429 -0
- onpar/prove/__init__.py +551 -0
- onpar/prove/__main__.py +12 -0
- onpar/prove/collect.py +953 -0
- onpar/prove/cost.py +259 -0
- onpar/prove/equivalence.py +799 -0
- onpar/prove/gate.py +702 -0
- onpar/prove/graders.py +569 -0
- onpar/prove/judge.py +390 -0
- onpar/prove/receipt.py +1012 -0
- onpar/prove/stats.py +660 -0
- onpar/sdk.py +334 -0
- onpar/session.py +965 -0
- onpar/ui.py +312 -0
- onpar/watch.py +440 -0
- onpar/workbench.html +408 -0
- onpar-1.3.0.dist-info/METADATA +102 -0
- onpar-1.3.0.dist-info/RECORD +57 -0
- onpar-1.3.0.dist-info/WHEEL +4 -0
- onpar-1.3.0.dist-info/entry_points.txt +3 -0
- onpar-1.3.0.dist-info/licenses/LICENSE +202 -0
onpar/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
def _detect_version() -> str:
|
|
2
|
+
"""The installed version, or the source tree's if this is not installed.
|
|
3
|
+
|
|
4
|
+
Metadata first: that is what the user actually has. The pyproject fallback
|
|
5
|
+
exists because the repo is normally run with `PYTHONPATH=src`, where no
|
|
6
|
+
distribution is installed and `PackageNotFoundError` would otherwise make
|
|
7
|
+
`--version` fail for every developer.
|
|
8
|
+
|
|
9
|
+
The distribution, the command and the import are all `onpar`.
|
|
10
|
+
"""
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
return version("onpar")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
pass
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
21
|
+
try:
|
|
22
|
+
m = re.search(r'^version\s*=\s*"([^"]+)"', pyproject.read_text(), re.M)
|
|
23
|
+
except OSError:
|
|
24
|
+
return "unknown"
|
|
25
|
+
return m.group(1) if m else "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__version__ = _detect_version()
|
onpar/advise.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""What you did not ask about — and what production is telling you.
|
|
2
|
+
|
|
3
|
+
[`plan`] answers the question asked: given these requirements, what engine and
|
|
4
|
+
what settings. It says what it set and what it *cannot* meet. What it never says
|
|
5
|
+
is what you are leaving on the table, because nothing in the requirements asked.
|
|
6
|
+
|
|
7
|
+
That gap is where most of the money is. Somebody deploys an agent fleet with a
|
|
8
|
+
2,000-token system prompt on every request, never sets a prefix-sharing figure
|
|
9
|
+
because no form demanded one, and pays to recompute the same prefix a million
|
|
10
|
+
times. The plan they got was correct. It was also 40% more expensive than it
|
|
11
|
+
needed to be, and nothing told them.
|
|
12
|
+
|
|
13
|
+
## Two directions
|
|
14
|
+
|
|
15
|
+
**Forward** — [`suggest`] reads the requirements and the plan and returns what a
|
|
16
|
+
careful reviewer would raise unprompted: the knob nobody set, the headroom nobody
|
|
17
|
+
spent, the budget that is about to be missed by a margin worth knowing.
|
|
18
|
+
|
|
19
|
+
**Backward** — [`reconcile`] takes what production actually did and compares it
|
|
20
|
+
to what the plan assumed. A plan built for concurrency 8 that is serving 40 is
|
|
21
|
+
not a plan any more, and the failure mode is that nobody notices until latency
|
|
22
|
+
complaints arrive. This is the self-healing seam: it proposes the correction,
|
|
23
|
+
with the evidence, and stops there.
|
|
24
|
+
|
|
25
|
+
## The rule
|
|
26
|
+
|
|
27
|
+
**Suggestions are proposals with evidence and an expected effect. Never actions.**
|
|
28
|
+
|
|
29
|
+
Every suggestion carries the observation that triggered it, so a wrong one is
|
|
30
|
+
visible and dismissible rather than mysterious. Nothing here mutates a plan,
|
|
31
|
+
writes a config, or restarts anything — the same boundary the rest of the
|
|
32
|
+
product holds. An agent may raise all of this; a human applies it.
|
|
33
|
+
|
|
34
|
+
Effects are labelled as estimates, because they are. A suggestion that promised
|
|
35
|
+
a measured 40% and delivered 4% would cost more trust than staying quiet.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from enum import StrEnum
|
|
42
|
+
|
|
43
|
+
from .plan import (
|
|
44
|
+
MAX_SPEC_DECODE_CONCURRENCY,
|
|
45
|
+
PREFIX_CACHING_FLOOR,
|
|
46
|
+
Plan,
|
|
47
|
+
Requirements,
|
|
48
|
+
Setting,
|
|
49
|
+
Workload,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"Impact",
|
|
54
|
+
"Observed",
|
|
55
|
+
"Suggestion",
|
|
56
|
+
"reconcile",
|
|
57
|
+
"suggest",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
#: Below this, a context allocation is so far above what the workload plausibly
|
|
61
|
+
#: sends that the KV cache is being sized for traffic nobody generates.
|
|
62
|
+
OVERSIZED_CONTEXT = 65_536
|
|
63
|
+
|
|
64
|
+
#: The smallest context worth recommending. Below this the advice is not
|
|
65
|
+
#: actionable — no engine is usefully started with a few hundred tokens — and
|
|
66
|
+
#: the observation behind it is too thin to be evidence of anything.
|
|
67
|
+
MIN_SERVING_CONTEXT = 2_048
|
|
68
|
+
|
|
69
|
+
#: Headroom fraction above which a better quantisation is worth proposing.
|
|
70
|
+
#: Deliberately generous: proposing an upgrade that then does not fit is worse
|
|
71
|
+
#: than staying quiet, because the user pays for the failed attempt in time.
|
|
72
|
+
COMFORTABLE_HEADROOM = 0.35
|
|
73
|
+
|
|
74
|
+
#: Concurrency and context at which prefill starts interfering with decode badly
|
|
75
|
+
#: enough that splitting them across instances is worth evaluating. Both must
|
|
76
|
+
#: hold: heavy concurrency with short prompts has little prefill to disaggregate,
|
|
77
|
+
#: and long prompts at low concurrency have nothing to collide with.
|
|
78
|
+
PD_DISAGG_CONCURRENCY = 32
|
|
79
|
+
PD_DISAGG_CONTEXT = 16_384
|
|
80
|
+
|
|
81
|
+
#: Concurrency this far above plan means the plan is describing a different
|
|
82
|
+
#: deployment. 1.5x rather than 1.05x — normal traffic varies, and a suggestion
|
|
83
|
+
#: that fires on noise gets filtered out mentally within a week.
|
|
84
|
+
CONCURRENCY_DRIFT = 1.5
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Impact(StrEnum):
|
|
88
|
+
"""How much a suggestion is worth, for ordering.
|
|
89
|
+
|
|
90
|
+
Three levels, not five. A scale finer than the evidence supports invites
|
|
91
|
+
arguing about whether something is a 3 or a 4 instead of acting on it.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
HIGH = "high"
|
|
95
|
+
MEDIUM = "medium"
|
|
96
|
+
LOW = "low"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_RANK = {Impact.HIGH: 0, Impact.MEDIUM: 1, Impact.LOW: 2}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class Suggestion:
|
|
104
|
+
"""One thing worth changing, why, and what it should buy."""
|
|
105
|
+
|
|
106
|
+
#: Short handle, stable enough to suppress by name.
|
|
107
|
+
id: str
|
|
108
|
+
impact: Impact
|
|
109
|
+
#: What to do, imperatively.
|
|
110
|
+
action: str
|
|
111
|
+
#: The observation that triggered this. Without it a suggestion is an
|
|
112
|
+
#: assertion, and an assertion cannot be argued with.
|
|
113
|
+
because: str
|
|
114
|
+
#: What it should buy, always hedged — these are estimates, not measurements.
|
|
115
|
+
expect: str
|
|
116
|
+
#: The knob this concerns, when there is one.
|
|
117
|
+
setting: Setting | None = None
|
|
118
|
+
|
|
119
|
+
def render(self) -> str:
|
|
120
|
+
head = f"[{self.impact.value}] {self.action}"
|
|
121
|
+
return f"{head}\n because: {self.because}\n expect: {self.expect}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class Observed:
|
|
126
|
+
"""What production actually did.
|
|
127
|
+
|
|
128
|
+
Every field is optional because telemetry arrives in pieces, and a
|
|
129
|
+
reconciliation that needs the full set before saying anything would say
|
|
130
|
+
nothing on the day it mattered.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
concurrency: int | None = None
|
|
134
|
+
#: Measured fraction of prompt tokens shared across requests.
|
|
135
|
+
prefix_sharing: float | None = None
|
|
136
|
+
#: Observed p95 time-to-first-token, milliseconds.
|
|
137
|
+
ttft_ms: int | None = None
|
|
138
|
+
#: Observed p95 inter-token latency, milliseconds.
|
|
139
|
+
itl_ms: int | None = None
|
|
140
|
+
#: Longest prompt actually seen, in tokens.
|
|
141
|
+
peak_context: int | None = None
|
|
142
|
+
#: Fraction of KV cache in use at peak, `0.0..=1.0`.
|
|
143
|
+
kv_utilisation: float | None = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def suggest(req: Requirements, p: Plan) -> list[Suggestion]:
|
|
147
|
+
"""What a careful reviewer would raise about this plan, unprompted.
|
|
148
|
+
|
|
149
|
+
Ordered by impact, then by the order found — stable, so the same inputs
|
|
150
|
+
produce the same list and a diff between two runs means something changed.
|
|
151
|
+
|
|
152
|
+
Returns an empty list when there is nothing worth saying. Padding it with
|
|
153
|
+
filler would train people to skim past the one that mattered.
|
|
154
|
+
"""
|
|
155
|
+
out: list[Suggestion] = []
|
|
156
|
+
|
|
157
|
+
# The single most under-used number in LLM serving. Nobody sets it because
|
|
158
|
+
# no form asks, and the default assumes every request is unique.
|
|
159
|
+
if req.prefix_sharing < PREFIX_CACHING_FLOOR and req.workload is not Workload.BATCH:
|
|
160
|
+
out.append(
|
|
161
|
+
Suggestion(
|
|
162
|
+
id="measure-prefix-sharing",
|
|
163
|
+
impact=Impact.HIGH,
|
|
164
|
+
action="Measure prefix sharing before accepting this plan.",
|
|
165
|
+
because=(
|
|
166
|
+
f"prefix_sharing is {req.prefix_sharing:.2f}, so prefix reuse is off. "
|
|
167
|
+
f"An agent or chat deployment with a fixed system prompt is commonly "
|
|
168
|
+
f"0.8+, and that prefix is being recomputed on every request."
|
|
169
|
+
),
|
|
170
|
+
expect=(
|
|
171
|
+
"if sharing is real, a large cut in prefill cost — the saving scales "
|
|
172
|
+
"with the shared fraction. Estimate, not measured: measure it."
|
|
173
|
+
),
|
|
174
|
+
setting=Setting.PREFIX_REUSE,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Context is the quietest way to overpay: KV scales linearly with it, and
|
|
179
|
+
# nobody revisits the number they typed on day one.
|
|
180
|
+
if req.context > OVERSIZED_CONTEXT and req.workload is Workload.INTERACTIVE:
|
|
181
|
+
out.append(
|
|
182
|
+
Suggestion(
|
|
183
|
+
id="right-size-context",
|
|
184
|
+
impact=Impact.MEDIUM,
|
|
185
|
+
action=f"Check whether {req.context:,} tokens of context is really needed.",
|
|
186
|
+
because=(
|
|
187
|
+
"KV cache scales linearly with context, and interactive traffic rarely "
|
|
188
|
+
"approaches a long ceiling. Serving a limit nobody reaches costs memory "
|
|
189
|
+
"that could hold concurrency instead."
|
|
190
|
+
),
|
|
191
|
+
expect=(
|
|
192
|
+
"halving context roughly halves KV memory, which buys either more "
|
|
193
|
+
"concurrent requests or a better quantisation. Roofline, not measured."
|
|
194
|
+
),
|
|
195
|
+
setting=Setting.CONTEXT_LENGTH,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Speculative decoding: the trap is that the published numbers are single-stream.
|
|
200
|
+
spec = p.get(Setting.SPECULATIVE)
|
|
201
|
+
if spec is not None and spec.value != "off" and req.concurrency > 8:
|
|
202
|
+
out.append(
|
|
203
|
+
Suggestion(
|
|
204
|
+
id="verify-speculative-at-your-batch",
|
|
205
|
+
impact=Impact.MEDIUM,
|
|
206
|
+
action="Benchmark speculative decoding at your real concurrency before keeping it.",
|
|
207
|
+
because=(
|
|
208
|
+
f"it is on at concurrency {req.concurrency}. Published EAGLE figures are "
|
|
209
|
+
f"single-stream; the win narrows as batch grows and published numbers "
|
|
210
|
+
f"turn negative around batch 32, which is why this plan switches it off "
|
|
211
|
+
f"above {MAX_SPEC_DECODE_CONCURRENCY} rather than at the crossover."
|
|
212
|
+
),
|
|
213
|
+
expect=(
|
|
214
|
+
"either a confirmed latency win or a throughput regression you would "
|
|
215
|
+
"otherwise have shipped. Measure both, at batch, not at 1."
|
|
216
|
+
),
|
|
217
|
+
setting=Setting.SPECULATIVE,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Spare memory is not a virtue. It is a quantisation you did not take.
|
|
222
|
+
fit = p.fit
|
|
223
|
+
if fit is not None and fit.feasible and fit.total_bytes > 0:
|
|
224
|
+
headroom = fit.headroom_bytes / (fit.total_bytes + fit.headroom_bytes)
|
|
225
|
+
if headroom > COMFORTABLE_HEADROOM:
|
|
226
|
+
out.append(
|
|
227
|
+
Suggestion(
|
|
228
|
+
id="spend-the-headroom",
|
|
229
|
+
impact=Impact.MEDIUM,
|
|
230
|
+
action="Try a higher-precision quantisation, or more context.",
|
|
231
|
+
because=(
|
|
232
|
+
f"{headroom * 100:.0f}% of usable memory is unallocated at this "
|
|
233
|
+
f"configuration. Idle VRAM buys nothing."
|
|
234
|
+
),
|
|
235
|
+
expect=(
|
|
236
|
+
"better output quality at the same throughput, or more concurrency. "
|
|
237
|
+
"Re-run fit to confirm the next step up actually fits."
|
|
238
|
+
),
|
|
239
|
+
setting=Setting.QUANTIZATION,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Prefill and decode want opposite things from a GPU, and in one process
|
|
244
|
+
# they fight. Splitting them is the 2026 answer — and it is a topology, not
|
|
245
|
+
# a flag, so this is a suggestion rather than something the planner emits.
|
|
246
|
+
tp = p.get(Setting.TENSOR_PARALLEL)
|
|
247
|
+
multi_device = bool(tp and isinstance(tp.value, int) and tp.value > 1)
|
|
248
|
+
if (
|
|
249
|
+
req.workload is not Workload.BATCH
|
|
250
|
+
and req.concurrency >= PD_DISAGG_CONCURRENCY
|
|
251
|
+
and req.context >= PD_DISAGG_CONTEXT
|
|
252
|
+
and multi_device
|
|
253
|
+
):
|
|
254
|
+
out.append(
|
|
255
|
+
Suggestion(
|
|
256
|
+
id="consider-pd-disaggregation",
|
|
257
|
+
impact=Impact.MEDIUM,
|
|
258
|
+
action="Evaluate prefill/decode disaggregation across two instances.",
|
|
259
|
+
because=(
|
|
260
|
+
f"concurrency {req.concurrency} at {req.context:,} context on "
|
|
261
|
+
f"{tp.value} devices. Long prefills and latency-sensitive decode "
|
|
262
|
+
f"share one GPU here, and they want opposite things from it — a "
|
|
263
|
+
f"prefill landing mid-batch is what puts variance in your "
|
|
264
|
+
f"time-to-first-token."
|
|
265
|
+
),
|
|
266
|
+
expect=(
|
|
267
|
+
"steadier tail latency, at the cost of real operational weight: "
|
|
268
|
+
"two vLLM deployments plus a routing proxy, KV moved between them "
|
|
269
|
+
"over --kv-transfer-config, and vLLM's own docs still label it "
|
|
270
|
+
"experimental and subject to change. Worth measuring before "
|
|
271
|
+
"adopting, not worth adopting on principle."
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# A latency budget nobody stated is a budget nobody can miss — and a
|
|
277
|
+
# deployment nobody can tell is failing.
|
|
278
|
+
if req.workload is not Workload.BATCH and req.ttft_ms is None and req.itl_ms is None:
|
|
279
|
+
out.append(
|
|
280
|
+
Suggestion(
|
|
281
|
+
id="state-a-latency-budget",
|
|
282
|
+
impact=Impact.LOW,
|
|
283
|
+
action="State a TTFT or ITL budget.",
|
|
284
|
+
because=(
|
|
285
|
+
f"{req.workload.value} traffic with no stated budget cannot be "
|
|
286
|
+
"checked against reality — the planner has nothing to warn about and "
|
|
287
|
+
"the guard has nothing to detect a regression from."
|
|
288
|
+
),
|
|
289
|
+
expect="warnings when the hardware cannot meet the budget, instead of silence.",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Structured output the engine cannot actually enforce is the worst kind of
|
|
294
|
+
# gap: the config looks right and the constraint silently is not applied.
|
|
295
|
+
if req.structured_output:
|
|
296
|
+
_, gaps = p.command("placeholder/model")
|
|
297
|
+
if any("structured" in g.lower() or "grammar" in g.lower() for g in gaps):
|
|
298
|
+
out.append(
|
|
299
|
+
Suggestion(
|
|
300
|
+
id="structured-output-unenforced",
|
|
301
|
+
impact=Impact.HIGH,
|
|
302
|
+
action="Validate outputs yourself, or pick an engine with a verified backend.",
|
|
303
|
+
because=(
|
|
304
|
+
"structured output was required but this engine has no verified flag "
|
|
305
|
+
"for it, so the emitted command does not enforce the schema."
|
|
306
|
+
),
|
|
307
|
+
expect="schema violations caught at your boundary rather than in production.",
|
|
308
|
+
setting=Setting.STRUCTURED_OUTPUT,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return sorted(out, key=lambda s: _RANK[s.impact])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def reconcile(req: Requirements, p: Plan, seen: Observed) -> list[Suggestion]:
|
|
316
|
+
"""Compare what production did against what the plan assumed.
|
|
317
|
+
|
|
318
|
+
This is the self-healing seam. It proposes; it never applies. A plan built
|
|
319
|
+
for one deployment and serving another is the common, silent failure — and
|
|
320
|
+
the only signal is usually a complaint.
|
|
321
|
+
|
|
322
|
+
Returns an empty list when reality matches the plan, which is the answer
|
|
323
|
+
most of the time and should read as reassuring rather than broken.
|
|
324
|
+
"""
|
|
325
|
+
out: list[Suggestion] = []
|
|
326
|
+
|
|
327
|
+
if seen.concurrency is not None and seen.concurrency > req.concurrency * CONCURRENCY_DRIFT:
|
|
328
|
+
out.append(
|
|
329
|
+
Suggestion(
|
|
330
|
+
id="replan-for-observed-concurrency",
|
|
331
|
+
impact=Impact.HIGH,
|
|
332
|
+
action=f"Re-plan for concurrency {seen.concurrency}.",
|
|
333
|
+
because=(
|
|
334
|
+
f"the plan assumes {req.concurrency} in-flight requests; production is "
|
|
335
|
+
f"running {seen.concurrency}. KV cache, batch limits and the speculative "
|
|
336
|
+
f"decision were all derived from the smaller number."
|
|
337
|
+
),
|
|
338
|
+
expect=(
|
|
339
|
+
"settings that match the traffic. At this concurrency the current plan "
|
|
340
|
+
"is describing a different deployment."
|
|
341
|
+
),
|
|
342
|
+
setting=Setting.MAX_CONCURRENT,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
seen.prefix_sharing is not None
|
|
348
|
+
and seen.prefix_sharing >= PREFIX_CACHING_FLOOR
|
|
349
|
+
and req.prefix_sharing < PREFIX_CACHING_FLOOR
|
|
350
|
+
):
|
|
351
|
+
out.append(
|
|
352
|
+
Suggestion(
|
|
353
|
+
id="enable-prefix-reuse",
|
|
354
|
+
impact=Impact.HIGH,
|
|
355
|
+
action="Turn on prefix reuse and re-plan.",
|
|
356
|
+
because=(
|
|
357
|
+
f"measured prefix sharing is {seen.prefix_sharing:.2f}, but the plan was "
|
|
358
|
+
f"built with {req.prefix_sharing:.2f} and left reuse off. That prefix is "
|
|
359
|
+
f"being recomputed on every request."
|
|
360
|
+
),
|
|
361
|
+
expect=(
|
|
362
|
+
f"prefill work cut roughly in proportion to the shared fraction "
|
|
363
|
+
f"({seen.prefix_sharing * 100:.0f}%). Measure it."
|
|
364
|
+
),
|
|
365
|
+
setting=Setting.PREFIX_REUSE,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if req.ttft_ms is not None and seen.ttft_ms is not None and seen.ttft_ms > req.ttft_ms:
|
|
370
|
+
out.append(
|
|
371
|
+
Suggestion(
|
|
372
|
+
id="ttft-budget-missed",
|
|
373
|
+
impact=Impact.HIGH,
|
|
374
|
+
action="Reduce context, raise prefill chunking, or accept a slower budget.",
|
|
375
|
+
because=(
|
|
376
|
+
f"p95 time-to-first-token is {seen.ttft_ms} ms against a stated budget of "
|
|
377
|
+
f"{req.ttft_ms} ms."
|
|
378
|
+
),
|
|
379
|
+
expect="a budget that is met, or a stated one that is honest about the hardware.",
|
|
380
|
+
setting=Setting.PREFILL_CHUNK,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if req.itl_ms is not None and seen.itl_ms is not None and seen.itl_ms > req.itl_ms:
|
|
385
|
+
out.append(
|
|
386
|
+
Suggestion(
|
|
387
|
+
id="itl-budget-missed",
|
|
388
|
+
impact=Impact.HIGH,
|
|
389
|
+
action="Lower concurrency, or move to a smaller model or faster quantisation.",
|
|
390
|
+
because=(
|
|
391
|
+
f"p95 inter-token latency is {seen.itl_ms} ms against a stated budget of "
|
|
392
|
+
f"{req.itl_ms} ms. Decode is memory-bound, so this is bytes per token, "
|
|
393
|
+
f"not compute."
|
|
394
|
+
),
|
|
395
|
+
expect="tokens arriving inside the budget, at some cost in throughput.",
|
|
396
|
+
setting=Setting.MAX_CONCURRENT,
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Context sized for traffic that never arrives — the reverse of the usual bug,
|
|
401
|
+
# and only visible once you have seen real prompts.
|
|
402
|
+
# `peak_context` of 0 is not evidence of over-provisioning — it is the
|
|
403
|
+
# absence of evidence, and it produced "Consider serving 0 tokens instead
|
|
404
|
+
# of 32,768". A peak of 1 produced "serve 2 tokens". Neither is a context
|
|
405
|
+
# any engine can be started with, and both read as confident advice.
|
|
406
|
+
#
|
|
407
|
+
# So: a floor on what may be recommended, and silence below it. A corpus
|
|
408
|
+
# whose longest prompt is under `MIN_SERVING_CONTEXT` has not shown that a
|
|
409
|
+
# smaller deployment would work; it has shown almost nothing.
|
|
410
|
+
if (
|
|
411
|
+
seen.peak_context is not None
|
|
412
|
+
and seen.peak_context * 2 < req.context
|
|
413
|
+
and seen.peak_context * 2 >= MIN_SERVING_CONTEXT
|
|
414
|
+
):
|
|
415
|
+
out.append(
|
|
416
|
+
Suggestion(
|
|
417
|
+
id="context-never-used",
|
|
418
|
+
impact=Impact.MEDIUM,
|
|
419
|
+
action=f"Consider serving {seen.peak_context * 2:,} tokens instead of "
|
|
420
|
+
f"{req.context:,}.",
|
|
421
|
+
because=(
|
|
422
|
+
f"the longest prompt actually seen is {seen.peak_context:,} tokens, less "
|
|
423
|
+
f"than half what is provisioned. The rest is KV memory held for traffic "
|
|
424
|
+
f"that has not arrived."
|
|
425
|
+
),
|
|
426
|
+
expect=(
|
|
427
|
+
"memory back for concurrency or quantisation. Keep a margin — this is "
|
|
428
|
+
"what was seen, not a ceiling."
|
|
429
|
+
),
|
|
430
|
+
setting=Setting.CONTEXT_LENGTH,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if seen.kv_utilisation is not None and seen.kv_utilisation > 0.9:
|
|
435
|
+
out.append(
|
|
436
|
+
Suggestion(
|
|
437
|
+
id="kv-pressure",
|
|
438
|
+
impact=Impact.HIGH,
|
|
439
|
+
action="Reduce context or concurrency before the cache starts evicting.",
|
|
440
|
+
because=(
|
|
441
|
+
f"KV cache is at {seen.kv_utilisation * 100:.0f}% at peak. Past full, the "
|
|
442
|
+
f"scheduler preempts requests, and the symptom is latency variance rather "
|
|
443
|
+
f"than an error anyone can grep for."
|
|
444
|
+
),
|
|
445
|
+
expect="stable tail latency instead of intermittent preemption.",
|
|
446
|
+
setting=Setting.MAX_CONCURRENT,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return sorted(out, key=lambda s: _RANK[s.impact])
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def demo() -> None:
|
|
454
|
+
from .hardware import Hardware
|
|
455
|
+
from .plan import plan
|
|
456
|
+
|
|
457
|
+
hw = Hardware(
|
|
458
|
+
kind="apple",
|
|
459
|
+
name="M4 Max",
|
|
460
|
+
total_bytes=128 * 1024**3,
|
|
461
|
+
usable_bytes=96 * 1024**3,
|
|
462
|
+
bandwidth_gbps=546.0,
|
|
463
|
+
cores=16,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# An agent fleet described the way people actually describe one: concurrency
|
|
467
|
+
# and context, and nothing about the system prompt they send every time.
|
|
468
|
+
req = Requirements(workload=Workload.INTERACTIVE, concurrency=16, context=131_072)
|
|
469
|
+
p = plan(hw, req)
|
|
470
|
+
s = suggest(req, p)
|
|
471
|
+
ids = [x.id for x in s]
|
|
472
|
+
|
|
473
|
+
assert "measure-prefix-sharing" in ids, ids
|
|
474
|
+
assert "right-size-context" in ids, ids
|
|
475
|
+
assert "state-a-latency-budget" in ids, ids
|
|
476
|
+
# Highest impact first, so the list can be truncated without losing the point.
|
|
477
|
+
assert s[0].impact is Impact.HIGH
|
|
478
|
+
assert [_RANK[x.impact] for x in s] == sorted(_RANK[x.impact] for x in s)
|
|
479
|
+
# Every suggestion carries its evidence and hedges its effect.
|
|
480
|
+
for x in s:
|
|
481
|
+
assert x.because and x.expect, x
|
|
482
|
+
|
|
483
|
+
# A well-specified deployment gets a short list, not a padded one.
|
|
484
|
+
tight = Requirements(
|
|
485
|
+
workload=Workload.INTERACTIVE,
|
|
486
|
+
concurrency=4,
|
|
487
|
+
context=8_192,
|
|
488
|
+
ttft_ms=400,
|
|
489
|
+
itl_ms=40,
|
|
490
|
+
prefix_sharing=0.8,
|
|
491
|
+
)
|
|
492
|
+
quiet = suggest(tight, plan(hw, tight))
|
|
493
|
+
assert "measure-prefix-sharing" not in {x.id for x in quiet}
|
|
494
|
+
assert "state-a-latency-budget" not in {x.id for x in quiet}
|
|
495
|
+
|
|
496
|
+
# Reality matching the plan says nothing at all.
|
|
497
|
+
assert reconcile(tight, plan(hw, tight), Observed(concurrency=4)) == []
|
|
498
|
+
|
|
499
|
+
# Reality diverging says exactly what diverged.
|
|
500
|
+
drifted = reconcile(
|
|
501
|
+
tight,
|
|
502
|
+
plan(hw, tight),
|
|
503
|
+
Observed(concurrency=40, ttft_ms=900, kv_utilisation=0.95, peak_context=2_000),
|
|
504
|
+
)
|
|
505
|
+
got = {x.id for x in drifted}
|
|
506
|
+
assert "replan-for-observed-concurrency" in got, got
|
|
507
|
+
assert "ttft-budget-missed" in got, got
|
|
508
|
+
assert "kv-pressure" in got, got
|
|
509
|
+
assert "context-never-used" in got, got
|
|
510
|
+
|
|
511
|
+
# Normal variance does not fire: 5 against a plan of 4 is traffic, not drift.
|
|
512
|
+
assert reconcile(tight, plan(hw, tight), Observed(concurrency=5)) == []
|
|
513
|
+
|
|
514
|
+
# Measured sharing the plan never knew about is the money case.
|
|
515
|
+
loose = Requirements(workload=Workload.INTERACTIVE, concurrency=4, context=8_192)
|
|
516
|
+
found = reconcile(loose, plan(hw, loose), Observed(prefix_sharing=0.85))
|
|
517
|
+
assert [x.id for x in found] == ["enable-prefix-reuse"], found
|
|
518
|
+
|
|
519
|
+
print(f"advise: {len(s)} suggestions forward, {len(drifted)} from observed reality")
|
|
520
|
+
for x in s:
|
|
521
|
+
print(f" {x.render()}")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
if __name__ == "__main__":
|
|
525
|
+
demo()
|