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.
Files changed (57) hide show
  1. onpar/__init__.py +28 -0
  2. onpar/advise.py +525 -0
  3. onpar/atomicio.py +220 -0
  4. onpar/box.py +1241 -0
  5. onpar/brief.py +341 -0
  6. onpar/cache.py +636 -0
  7. onpar/catalog.py +339 -0
  8. onpar/catalog_update.py +625 -0
  9. onpar/cli.py +2463 -0
  10. onpar/core.py +167 -0
  11. onpar/desktop.py +248 -0
  12. onpar/distill/__init__.py +0 -0
  13. onpar/distill/cluster.py +271 -0
  14. onpar/distill/name.py +329 -0
  15. onpar/distill/shape.py +336 -0
  16. onpar/engine.py +119 -0
  17. onpar/engines.py +1197 -0
  18. onpar/events.py +213 -0
  19. onpar/fit.py +685 -0
  20. onpar/guard.py +436 -0
  21. onpar/hardware.py +406 -0
  22. onpar/hardware_catalog.py +250 -0
  23. onpar/host.py +1537 -0
  24. onpar/intent.py +1087 -0
  25. onpar/k8s/__init__.py +0 -0
  26. onpar/k8s/controller.py +403 -0
  27. onpar/k8s/nodes.py +523 -0
  28. onpar/k8s/reconcile.py +633 -0
  29. onpar/kernels.py +419 -0
  30. onpar/launch.py +1128 -0
  31. onpar/mcp.py +1303 -0
  32. onpar/measure.py +689 -0
  33. onpar/migrate.py +425 -0
  34. onpar/models.json +291 -0
  35. onpar/observe.py +355 -0
  36. onpar/plan.py +900 -0
  37. onpar/posttrain.py +429 -0
  38. onpar/prove/__init__.py +551 -0
  39. onpar/prove/__main__.py +12 -0
  40. onpar/prove/collect.py +953 -0
  41. onpar/prove/cost.py +259 -0
  42. onpar/prove/equivalence.py +799 -0
  43. onpar/prove/gate.py +702 -0
  44. onpar/prove/graders.py +569 -0
  45. onpar/prove/judge.py +390 -0
  46. onpar/prove/receipt.py +1012 -0
  47. onpar/prove/stats.py +660 -0
  48. onpar/sdk.py +334 -0
  49. onpar/session.py +965 -0
  50. onpar/ui.py +312 -0
  51. onpar/watch.py +440 -0
  52. onpar/workbench.html +408 -0
  53. onpar-1.3.0.dist-info/METADATA +102 -0
  54. onpar-1.3.0.dist-info/RECORD +57 -0
  55. onpar-1.3.0.dist-info/WHEEL +4 -0
  56. onpar-1.3.0.dist-info/entry_points.txt +3 -0
  57. 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()