inferencekit-cli 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.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
"""intake template / intake validate / intake budget: the input of the optimizing-inference flow.
|
|
2
|
+
|
|
3
|
+
An intake file (``intake.yaml``) says what to make fast (model + pinned revision +
|
|
4
|
+
allowed precisions), on what hardware and how to reach it, for which goal
|
|
5
|
+
(single-user speed or capacity, with optional targets), with which input/output
|
|
6
|
+
modes and buckets, and inside which approved budget. The orchestrator skill reads it
|
|
7
|
+
once at stage 1 and never changes it afterwards: the targets in it are the
|
|
8
|
+
pre-registered claims the final report is judged against.
|
|
9
|
+
|
|
10
|
+
* ``intake template`` prints an annotated YAML file to start from.
|
|
11
|
+
* ``intake validate <file>`` checks it against ``schemas/intake.v1.json`` plus the
|
|
12
|
+
rules a schema cannot express (an unattended run needs an approved budget, a
|
|
13
|
+
capacity goal needs an SLO, a lossy run needs a quality budget, ...), fills defaults and
|
|
14
|
+
returns the normalized intake with warnings and the derived plan (stop rule, fidelity rule
|
|
15
|
+
and quality budget, speculation and route, budget cap, per-stage time boxes).
|
|
16
|
+
With ``--run-dir`` it also starts (or resumes) ``runs/<run-id>/``: a redacted copy of
|
|
17
|
+
the intake (access details removed), ``intake.normalized.json``, ``state.json`` and
|
|
18
|
+
``QUESTIONS.md``. A run whose recorded intake_sha256 differs is refused: the targets
|
|
19
|
+
are pre-registered, so a changed intake needs a new run id.
|
|
20
|
+
* ``intake budget <run-dir>`` reads ``state.json`` and says how much of the budget and
|
|
21
|
+
of the current stage's time box is spent. Read-only; the skill runs it before every
|
|
22
|
+
long step.
|
|
23
|
+
|
|
24
|
+
No numbers here are measurements: the budget and time-box figures are arithmetic on
|
|
25
|
+
the intake and the clock (claim class ``derived``), so no evidence record is written.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import datetime as _dt
|
|
31
|
+
import json
|
|
32
|
+
import math
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
from importlib import resources
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from inferencekit.canonical import sha256_hex
|
|
39
|
+
from inferencekit.registry import Arg, OpError, operation, utc_now
|
|
40
|
+
|
|
41
|
+
BUCKETS = ("very_short", "short", "medium", "long")
|
|
42
|
+
|
|
43
|
+
# Stage keys of state.json, in order, with the share of the run's hour cap each one gets.
|
|
44
|
+
# The report keeps a fixed reserve so a run that hits its budget still ends with a report.
|
|
45
|
+
STAGES = (
|
|
46
|
+
("1_intake", 0.02),
|
|
47
|
+
("2_hardware", 0.05),
|
|
48
|
+
("3_candidates", 0.18),
|
|
49
|
+
("4_tuning", 0.22),
|
|
50
|
+
("5_benchmark", 0.18),
|
|
51
|
+
("6_stop_point", 0.02),
|
|
52
|
+
("7_custom", 0.23),
|
|
53
|
+
("8_report", 0.10),
|
|
54
|
+
)
|
|
55
|
+
REPORT_RESERVE = dict(STAGES)["8_report"]
|
|
56
|
+
|
|
57
|
+
# name -> (better direction, unit)
|
|
58
|
+
TARGETS = {
|
|
59
|
+
"decode_tok_s": ("higher", "tok/s"),
|
|
60
|
+
"ttft_ms": ("lower", "ms"),
|
|
61
|
+
"n_star": ("higher", "streams/GPU"),
|
|
62
|
+
"decode_ceiling_fraction": ("higher", "fraction of roofline ceiling"),
|
|
63
|
+
"ttfa_ms": ("lower", "ms"),
|
|
64
|
+
"ttfa_p95_ms": ("lower", "ms"),
|
|
65
|
+
"image_ms": ("lower", "ms per image"),
|
|
66
|
+
"images_per_hour": ("higher", "images/h"),
|
|
67
|
+
"rtf": ("lower", "x real time"),
|
|
68
|
+
"usd_per_1m": ("lower", "USD per 1M units"),
|
|
69
|
+
"clip_ms": ("lower", "ms per clip"),
|
|
70
|
+
"clips_per_hour": ("higher", "clips/h"),
|
|
71
|
+
}
|
|
72
|
+
MODALITY_TARGETS = {
|
|
73
|
+
"llm": {"decode_tok_s", "ttft_ms", "n_star", "decode_ceiling_fraction", "usd_per_1m"},
|
|
74
|
+
"tts": {"ttfa_ms", "ttfa_p95_ms", "rtf", "n_star", "usd_per_1m"},
|
|
75
|
+
"stt": {"rtf", "n_star", "usd_per_1m"},
|
|
76
|
+
"video": {"clip_ms", "clips_per_hour"},
|
|
77
|
+
"image": {"image_ms", "images_per_hour"},
|
|
78
|
+
}
|
|
79
|
+
DEFAULT_CONCURRENCY = [1, 2, 4, 8, 16, 32, 64]
|
|
80
|
+
ROOFLINE_DTYPES = {"bf16", "fp16", "fp8", "q8_0"}
|
|
81
|
+
# Run rules a constraint string can set ("after_target: keep_climbing"): name -> allowed values. after_target and
|
|
82
|
+
# fidelity are also schema fields (goals.after_target, fidelity); a field and a constraint must agree.
|
|
83
|
+
RULES = {"after_target": ("stop", "keep_climbing"), "fidelity": ("lossless", "lossy"),
|
|
84
|
+
"speculation": ("off", "auto"), "route": ("build", "patch")}
|
|
85
|
+
_RULE_NAME = r"(after[\s_-]?target|fidelity|speculation|route)"
|
|
86
|
+
_RULE = re.compile(rf"^\s*{_RULE_NAME}\s*:\s*(.*)$", re.IGNORECASE | re.DOTALL)
|
|
87
|
+
_RULE_INSIDE = re.compile(rf"(?<![\w-]){_RULE_NAME}\s*:", re.IGNORECASE)
|
|
88
|
+
# Budget keys that fix how long a run may go (with keep_climbing they are where it ends).
|
|
89
|
+
CAP_KEYS = ("gpu_hours", "usd", "wall_clock_h")
|
|
90
|
+
QUALITY_BUDGET_KEYS = ("gsm8k_drop_pts_max", "top1_agreement_min", "kl_max", "nll_increase_max", "tv_max")
|
|
91
|
+
# Video (quality clip --fidelity lossy --budget): full-reference bounds on the engine's mean vs the BF16 reference.
|
|
92
|
+
VIDEO_BUDGET_KEYS = ("psnr_db_min", "ssim_min", "lpips_max")
|
|
93
|
+
SPEECH_BUDGET_KEYS = ("wer_max", "cer_max", "speaker_similarity_min")
|
|
94
|
+
# Degradation limits evaluated by a hash-bound external modality protocol, not by
|
|
95
|
+
# the built-in absolute scorers. Positive increase/drop is always worse.
|
|
96
|
+
RELATIVE_BUDGET_KEYS = {
|
|
97
|
+
"image": ("lpips_excess_max", "ssim_loss_max", "ocr_cer_increase_max",
|
|
98
|
+
"semantic_score_drop_max", "new_severe_regressions_max"),
|
|
99
|
+
"tts": ("wer_increase_max", "cer_increase_max", "completion_failure_increase_max",
|
|
100
|
+
"speaker_similarity_drop_max", "perceptual_score_drop_max", "new_severe_regressions_max"),
|
|
101
|
+
"stt": ("wer_increase_max", "cer_increase_max", "completion_failure_increase_max",
|
|
102
|
+
"new_severe_regressions_max"),
|
|
103
|
+
}
|
|
104
|
+
PERCEPTUAL_ACCEPTANCE = "perceptual_and_task_noninferiority"
|
|
105
|
+
PERCEPTUAL_BUDGET_KEYS = {
|
|
106
|
+
"image": ("ocr_cer_increase_max", "semantic_score_drop_max", "new_severe_regressions_max"),
|
|
107
|
+
"tts": RELATIVE_BUDGET_KEYS["tts"],
|
|
108
|
+
}
|
|
109
|
+
_DRIFT_DIAGNOSTICS = ("cross_engine_byte_identity", "cross_engine_tensor_equality", "rounding_drift",
|
|
110
|
+
"eager_compiled_drift", "repeated_run_variation")
|
|
111
|
+
DIAGNOSTIC_QUALITY_METRICS = {
|
|
112
|
+
"image": ("lpips", "ssim", "psnr", *_DRIFT_DIAGNOSTICS),
|
|
113
|
+
"tts": ("cross_engine_pcm_equality", "cross_engine_generated_code_equality", *_DRIFT_DIAGNOSTICS),
|
|
114
|
+
}
|
|
115
|
+
# KV cache dtypes below 16 bits. Under fidelity lossless they change numerics, unless the checkpoint itself specifies
|
|
116
|
+
# one (then the intake says checkpoint_default).
|
|
117
|
+
_LOSSY_KV = re.compile(r"^(u?int[248]|fp[468]|float[48]|f8|e[2-5]m[0-9]|nvfp4|mxfp[468]|i?q[2-8])(?![0-9])",
|
|
118
|
+
re.IGNORECASE)
|
|
119
|
+
# <angle>, CHANGEME, TODO, and __UPPER_SNAKE__ fill-in markers (case-sensitive, so __init__ is not one).
|
|
120
|
+
_PLACEHOLDER = re.compile(r"<[^<>]+>|\bCHANGE[_-]?ME\b|\bTODO\b|(?-i:(?<![A-Za-z0-9])__[A-Z][A-Z0-9_]*__(?![A-Za-z0-9]))",
|
|
121
|
+
re.IGNORECASE)
|
|
122
|
+
REDACTED = "<redacted>"
|
|
123
|
+
_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --------------------------------------------------------------------------- loading
|
|
127
|
+
|
|
128
|
+
def schema_path() -> Path:
|
|
129
|
+
"""The schema shipped in the wheel, or the repo copy in a source checkout."""
|
|
130
|
+
try:
|
|
131
|
+
packaged = resources.files("inferencekit").joinpath("schemas/intake.v1.json")
|
|
132
|
+
if packaged.is_file():
|
|
133
|
+
return Path(str(packaged))
|
|
134
|
+
except (ModuleNotFoundError, FileNotFoundError):
|
|
135
|
+
pass
|
|
136
|
+
return Path(__file__).resolve().parents[3] / "schemas" / "intake.v1.json"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_schema() -> dict:
|
|
140
|
+
p = schema_path()
|
|
141
|
+
if not p.is_file():
|
|
142
|
+
raise OpError(f"intake schema not found at {p}: the wheel must ship schemas/intake.v1.json")
|
|
143
|
+
return json.loads(p.read_text())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_text(text: str, name: str = "intake") -> dict:
|
|
147
|
+
"""YAML (or JSON, which is valid YAML) to a dict."""
|
|
148
|
+
stripped = text.lstrip()
|
|
149
|
+
if stripped.startswith("{"):
|
|
150
|
+
try:
|
|
151
|
+
data = json.loads(text)
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
raise OpError(f"{name}: invalid JSON: {e}") from None
|
|
154
|
+
else:
|
|
155
|
+
try:
|
|
156
|
+
import yaml
|
|
157
|
+
except ImportError:
|
|
158
|
+
raise OpError("reading YAML needs PyYAML (pip install pyyaml); a JSON intake works without it") from None
|
|
159
|
+
try:
|
|
160
|
+
data = yaml.safe_load(text)
|
|
161
|
+
except yaml.YAMLError as e:
|
|
162
|
+
raise OpError(f"{name}: invalid YAML: {e}") from None
|
|
163
|
+
if not isinstance(data, dict):
|
|
164
|
+
raise OpError(f"{name}: the top level must be a mapping (key: value), got {type(data).__name__}")
|
|
165
|
+
return data
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_file(path: str | os.PathLike) -> tuple[dict, bytes]:
|
|
169
|
+
p = Path(path)
|
|
170
|
+
if not p.is_file():
|
|
171
|
+
raise OpError(f"intake file not found: {p}")
|
|
172
|
+
raw = p.read_bytes()
|
|
173
|
+
try:
|
|
174
|
+
text = raw.decode("utf-8")
|
|
175
|
+
except UnicodeDecodeError:
|
|
176
|
+
raise OpError(f"{p}: not UTF-8 text") from None
|
|
177
|
+
return parse_text(text, p.name), raw
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------- checking
|
|
181
|
+
|
|
182
|
+
def _dotted(path) -> str:
|
|
183
|
+
return ".".join(str(p) for p in path) or "(top level)"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def schema_problems(data: dict) -> tuple[list[str], list[str]]:
|
|
187
|
+
"""(errors, warnings). Unknown keys are warnings: probably a typo, never silently used."""
|
|
188
|
+
import jsonschema
|
|
189
|
+
|
|
190
|
+
errors, warnings = [], []
|
|
191
|
+
validator = jsonschema.Draft202012Validator(load_schema())
|
|
192
|
+
for err in sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path)):
|
|
193
|
+
where = _dotted(err.absolute_path)
|
|
194
|
+
if err.validator == "additionalProperties":
|
|
195
|
+
known = sorted((err.schema.get("properties") or {}).keys())
|
|
196
|
+
extra = sorted(set(err.instance) - set(known)) if isinstance(err.instance, dict) else []
|
|
197
|
+
if list(err.absolute_path)[:3] == ["quality", "budget", "relative"]:
|
|
198
|
+
errors.append(f"{where}: unknown relative budget key(s) {extra}; known keys: {known}")
|
|
199
|
+
else:
|
|
200
|
+
warnings.append(f"{where}: unknown key(s) {extra} ignored (typo?); known keys: {known}")
|
|
201
|
+
elif err.validator == "oneOf":
|
|
202
|
+
errors.append(f"{where}: {err.instance!r} is not a valid value "
|
|
203
|
+
f"({err.schema.get('description') or 'see schemas/intake.v1.json'})")
|
|
204
|
+
else:
|
|
205
|
+
errors.append(f"{where}: {err.message}")
|
|
206
|
+
return errors, warnings
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _drop_unknown(data, schema):
|
|
210
|
+
"""Remove keys the schema does not define (they were reported as warnings)."""
|
|
211
|
+
if not isinstance(data, dict) or not isinstance(schema, dict):
|
|
212
|
+
return data
|
|
213
|
+
props = schema.get("properties")
|
|
214
|
+
if props is None or schema.get("additionalProperties", True) is not False:
|
|
215
|
+
return data
|
|
216
|
+
return {k: _drop_unknown(v, props[k]) for k, v in data.items() if k in props}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def placeholders(node, path=()) -> list[str]:
|
|
220
|
+
"""Dotted paths of string values that still look like template placeholders."""
|
|
221
|
+
out = []
|
|
222
|
+
if isinstance(node, dict):
|
|
223
|
+
for k, v in node.items():
|
|
224
|
+
out += placeholders(v, path + (k,))
|
|
225
|
+
elif isinstance(node, list):
|
|
226
|
+
for i, v in enumerate(node):
|
|
227
|
+
out += placeholders(v, path + (i,))
|
|
228
|
+
elif isinstance(node, str) and _PLACEHOLDER.search(node):
|
|
229
|
+
out.append(_dotted(path))
|
|
230
|
+
return out
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _slug(text: str) -> str:
|
|
234
|
+
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")[:40] or "x"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def constraint_rules(constraints) -> tuple[dict, list[str]]:
|
|
238
|
+
"""Run rules set by constraint strings, e.g. ``after_target: keep_climbing`` or ``speculation: off``.
|
|
239
|
+
|
|
240
|
+
Returns ({name: value}, errors). An entry may hold several rules separated by ``;``. The name may be written
|
|
241
|
+
with ``_``, ``-``, a space or nothing between its words ("after target"); the value is read at the start of
|
|
242
|
+
the text after the colon, the same way ("keep climbing"), so a reason may follow it ("speculation: off, the
|
|
243
|
+
task forbids it"). A recognised name with an unknown value, a second rule inside a reason, or two entries that
|
|
244
|
+
disagree, is an error: a typo here would otherwise be ignored silently.
|
|
245
|
+
"""
|
|
246
|
+
found, errors = {}, []
|
|
247
|
+
for text in constraints or []:
|
|
248
|
+
for part in text.split(";") if isinstance(text, str) else []:
|
|
249
|
+
m = _RULE.match(part)
|
|
250
|
+
if not m:
|
|
251
|
+
continue
|
|
252
|
+
name = "after_target" if m.group(1).lower().startswith("after") else m.group(1).lower()
|
|
253
|
+
rest = m.group(2).strip()
|
|
254
|
+
if _RULE_INSIDE.search(rest):
|
|
255
|
+
errors.append(f"constraints: {part.strip()!r} holds more than one rule: give each rule its own "
|
|
256
|
+
"entry, or separate them with ';'")
|
|
257
|
+
continue
|
|
258
|
+
value = next((v for v in RULES[name]
|
|
259
|
+
if re.match(v.replace("_", r"[\s_-]+") + r"(?![\w-])", rest, re.IGNORECASE)), "")
|
|
260
|
+
if value not in RULES[name]:
|
|
261
|
+
errors.append(f"constraints: {part.strip()!r}: {name} must be one of {list(RULES[name])}")
|
|
262
|
+
elif found.setdefault(name, value) != value:
|
|
263
|
+
errors.append(f"constraints: {name} is given twice with different values "
|
|
264
|
+
f"({found[name]!r} and {value!r}); keep one")
|
|
265
|
+
return found, errors
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def normalize(data: dict) -> tuple[dict, list[str], list[str]]:
|
|
269
|
+
"""Fill defaults and apply the cross-field rules. Returns (intake, errors, warnings)."""
|
|
270
|
+
errors, warnings = [], []
|
|
271
|
+
d = json.loads(json.dumps(data)) # deep copy, JSON types only
|
|
272
|
+
|
|
273
|
+
model = d["model"]
|
|
274
|
+
if "precision_allowed" not in model:
|
|
275
|
+
if "precision" in model:
|
|
276
|
+
model["precision_allowed"] = [model["precision"]]
|
|
277
|
+
else:
|
|
278
|
+
errors.append("model: give precision (the checkpoint's own) and/or precision_allowed")
|
|
279
|
+
model["precision_allowed"] = []
|
|
280
|
+
if "precision" in model and model["precision_allowed"] and model["precision"] not in model["precision_allowed"]:
|
|
281
|
+
warnings.append(f"model.precision {model['precision']!r} is not in model.precision_allowed "
|
|
282
|
+
f"{model['precision_allowed']}: every ranked engine converts the checkpoint; the "
|
|
283
|
+
"conversion itself must pass the quality gates")
|
|
284
|
+
if len(model.get("precision_allowed", [])) > 1:
|
|
285
|
+
warnings.append(f"model.precision_allowed has {len(model['precision_allowed'])} entries: each precision "
|
|
286
|
+
"is its own track; rows are never ranked across precisions")
|
|
287
|
+
model.setdefault("kv_cache_dtype_allowed", ["checkpoint_default"])
|
|
288
|
+
model.setdefault("tokenizer", model["id"])
|
|
289
|
+
rev = model.get("revision")
|
|
290
|
+
if rev is None:
|
|
291
|
+
warnings.append("model.revision missing: stage 1 resolves the current commit sha and records it; "
|
|
292
|
+
"every engine must load that exact revision")
|
|
293
|
+
elif not _SHA.match(rev) and not _PLACEHOLDER.search(rev):
|
|
294
|
+
warnings.append(f"model.revision {rev!r} is not a 40-hex commit sha: stage 1 resolves it to one and "
|
|
295
|
+
"records it")
|
|
296
|
+
|
|
297
|
+
hw = d["hardware"]
|
|
298
|
+
hw.setdefault("count", 1)
|
|
299
|
+
access = hw.setdefault("access", {"kind": "local"})
|
|
300
|
+
if access.get("kind") == "ssh":
|
|
301
|
+
access.setdefault("port", 22)
|
|
302
|
+
key = access.get("key_path")
|
|
303
|
+
if key and not _PLACEHOLDER.search(key) and not Path(os.path.expanduser(key)).exists():
|
|
304
|
+
warnings.append("hardware.access.key_path does not exist on this machine")
|
|
305
|
+
|
|
306
|
+
modality = d["modality"]
|
|
307
|
+
goals = d.setdefault("goals", {})
|
|
308
|
+
goals.setdefault("lead", "single_user")
|
|
309
|
+
affordability = goals.get("affordability")
|
|
310
|
+
if goals["lead"] == "affordability" and not affordability:
|
|
311
|
+
errors.append("goals.lead affordability requires goals.affordability memory limits")
|
|
312
|
+
if affordability:
|
|
313
|
+
affordability.setdefault("offload_allowed", False)
|
|
314
|
+
stretch = affordability.get("stretch_memory_gb_max")
|
|
315
|
+
if stretch is not None and stretch >= affordability["memory_gb_max"]:
|
|
316
|
+
errors.append("goals.affordability.stretch_memory_gb_max must be smaller than memory_gb_max")
|
|
317
|
+
targets = goals.setdefault("targets", {})
|
|
318
|
+
slo = goals.setdefault("slo", {})
|
|
319
|
+
slo.setdefault("max_fail", 0)
|
|
320
|
+
goals.setdefault("traffic", {})
|
|
321
|
+
bad = sorted(set(targets) - {"buckets"} - MODALITY_TARGETS[modality])
|
|
322
|
+
if bad:
|
|
323
|
+
warnings.append(f"goals.targets {bad} do not apply to modality {modality} "
|
|
324
|
+
f"(use {sorted(MODALITY_TARGETS[modality])}); they are reported NOT RUN")
|
|
325
|
+
needs_load = goals["lead"] == "capacity" or "n_star" in targets
|
|
326
|
+
if needs_load and "ttft_p95_ms" not in slo:
|
|
327
|
+
errors.append("goals.slo.ttft_p95_ms is required when the lead goal is capacity or a target is n_star "
|
|
328
|
+
"(N* is defined by the SLO)")
|
|
329
|
+
if needs_load and modality == "llm" and "tpot_p95_ms" not in slo:
|
|
330
|
+
warnings.append("goals.slo.tpot_p95_ms missing: N* will hold only the first-token SLO and failures")
|
|
331
|
+
if "n_star" in targets and goals["lead"] != "capacity":
|
|
332
|
+
warnings.append("goals.targets.n_star is set but lead is single_user: the capacity (load) path runs too")
|
|
333
|
+
frac = targets.get("decode_ceiling_fraction")
|
|
334
|
+
if frac is not None and frac > 0.9:
|
|
335
|
+
warnings.append(f"goals.targets.decode_ceiling_fraction {frac} is above 0.9 of a measured-bandwidth "
|
|
336
|
+
"ceiling; few engines get there at batch 1")
|
|
337
|
+
if frac is not None and not set(model["precision_allowed"]) <= ROOFLINE_DTYPES:
|
|
338
|
+
warnings.append("goals.targets.decode_ceiling_fraction: 'inferencekit roofline' covers dense "
|
|
339
|
+
f"{sorted(ROOFLINE_DTYPES)} full-attention models only; for {model['precision_allowed']} (or MoE, "
|
|
340
|
+
"sliding-window or hybrid attention) stage 2 derives bytes/token from the checkpoint with the "
|
|
341
|
+
"skill's scripts/bytes_per_token.py and records the formula")
|
|
342
|
+
|
|
343
|
+
io = d.setdefault("io_modes", {})
|
|
344
|
+
io.setdefault("input", "complete")
|
|
345
|
+
io.setdefault("output", "streaming")
|
|
346
|
+
if io["input"] == "streaming":
|
|
347
|
+
warnings.append("io_modes.input is streaming: engines without incremental input are NOT SUPPORTED for "
|
|
348
|
+
"it; any client-side emulation is labelled as emulation")
|
|
349
|
+
if "input_rate" not in io:
|
|
350
|
+
warnings.append("io_modes.input_rate missing: the replay pace of streaming input is unknown")
|
|
351
|
+
|
|
352
|
+
d.setdefault("buckets", list(BUCKETS))
|
|
353
|
+
d["buckets"] = [b for b in BUCKETS if b in d["buckets"]]
|
|
354
|
+
if modality in ("image", "video"): # modality workload set, not text length buckets
|
|
355
|
+
if targets.pop("buckets", None) is not None:
|
|
356
|
+
warnings.append(f"goals.targets.buckets do not apply to modality {modality}: a target holds over the "
|
|
357
|
+
"whole modality workload set")
|
|
358
|
+
elif "buckets" in targets:
|
|
359
|
+
outside = sorted(set(targets["buckets"]) - set(d["buckets"]))
|
|
360
|
+
if outside:
|
|
361
|
+
errors.append(f"goals.targets.buckets {outside} are not in buckets {d['buckets']}")
|
|
362
|
+
elif any(k != "buckets" for k in targets):
|
|
363
|
+
targets["buckets"] = list(d["buckets"])
|
|
364
|
+
|
|
365
|
+
wl = d.setdefault("workload", {})
|
|
366
|
+
wl.setdefault("max_tokens", 256)
|
|
367
|
+
wl.setdefault("rounds", 5)
|
|
368
|
+
wl.setdefault("prompt_set", "dev")
|
|
369
|
+
if needs_load:
|
|
370
|
+
wl.setdefault("concurrency", list(DEFAULT_CONCURRENCY))
|
|
371
|
+
if "concurrency" in wl:
|
|
372
|
+
wl["concurrency"] = sorted(set(wl["concurrency"]))
|
|
373
|
+
|
|
374
|
+
budget = d.setdefault("budget", {})
|
|
375
|
+
budget.setdefault("approved", False)
|
|
376
|
+
uncapped = budget.get("spend_cap") == "none"
|
|
377
|
+
if uncapped:
|
|
378
|
+
if budget.get("usd") is not None or budget.get("gpu_hours") is not None:
|
|
379
|
+
errors.append("budget.spend_cap: none conflicts with budget.usd or budget.gpu_hours")
|
|
380
|
+
if not budget["approved"]:
|
|
381
|
+
errors.append("budget.spend_cap: none requires budget.approved: true")
|
|
382
|
+
if not budget.get("deadline") and not budget.get("wall_clock_h"):
|
|
383
|
+
errors.append("budget.spend_cap: none requires a finite budget.deadline or budget.wall_clock_h")
|
|
384
|
+
if budget.get("deadline"):
|
|
385
|
+
try:
|
|
386
|
+
deadline = _dt.datetime.fromisoformat(budget["deadline"].replace("Z", "+00:00"))
|
|
387
|
+
if deadline.tzinfo is None:
|
|
388
|
+
raise ValueError("timezone required")
|
|
389
|
+
except ValueError:
|
|
390
|
+
errors.append("budget.deadline must be an ISO8601 time with a timezone")
|
|
391
|
+
price = hw.get("price_usd_h")
|
|
392
|
+
has_cap = "gpu_hours" in budget or ("usd" in budget and price)
|
|
393
|
+
if "usd" in budget and not price:
|
|
394
|
+
(errors if d.get("non_interactive") else warnings).append(
|
|
395
|
+
"budget.usd is set but hardware.price_usd_h is not: dollars cannot be turned into hours")
|
|
396
|
+
if d.get("non_interactive"):
|
|
397
|
+
if not budget["approved"]:
|
|
398
|
+
errors.append("non_interactive: true needs budget.approved: true (an unattended run may only spend an "
|
|
399
|
+
"approved budget)")
|
|
400
|
+
if not has_cap and not uncapped:
|
|
401
|
+
errors.append("non_interactive: true needs a spend cap: budget.gpu_hours, or budget.usd with "
|
|
402
|
+
"hardware.price_usd_h")
|
|
403
|
+
elif not budget["approved"]:
|
|
404
|
+
warnings.append("budget.approved is false: every GPU-occupying step asks first")
|
|
405
|
+
|
|
406
|
+
q = d.setdefault("quality", {})
|
|
407
|
+
ref = q.get("reference", "none")
|
|
408
|
+
if ref == "none" or (isinstance(ref, dict) and ref.get("kind") == "none"):
|
|
409
|
+
ref = {"kind": "none"}
|
|
410
|
+
warnings.append("quality.reference is none: agreement is measured against the baseline engine at the same "
|
|
411
|
+
"precision, not against a higher-precision reference")
|
|
412
|
+
q["reference"] = ref
|
|
413
|
+
if goals["lead"] == "affordability":
|
|
414
|
+
if not q.get("protocol"):
|
|
415
|
+
errors.append("goals.lead affordability requires quality.protocol with modality quality gates")
|
|
416
|
+
if not any(TARGETS[k][0] == "lower" and k != "usd_per_1m" for k in targets if k in MODALITY_TARGETS[modality]):
|
|
417
|
+
errors.append("goals.lead affordability requires an explicit latency target in goals.targets")
|
|
418
|
+
q.setdefault("noise_floor", "measure")
|
|
419
|
+
|
|
420
|
+
repo = d.get("repo")
|
|
421
|
+
if repo:
|
|
422
|
+
if not repo.get("path"):
|
|
423
|
+
warnings.append("repo.path missing: interactive runs ask which repo to use; non_interactive runs skip "
|
|
424
|
+
"the ledger (NOT RUN)")
|
|
425
|
+
repo.setdefault("push", "ask")
|
|
426
|
+
repo.setdefault("branch", f"perf/{_slug(model['id'].split('/')[-1])}-{_slug(hw['gpu'])}")
|
|
427
|
+
else:
|
|
428
|
+
d["repo"] = None
|
|
429
|
+
warnings.append("repo missing: kept changes are not committed; the commit ledger is NOT RUN")
|
|
430
|
+
|
|
431
|
+
cons = d.get("constraints")
|
|
432
|
+
d["constraints"] = [cons] if isinstance(cons, str) else list(cons or [])
|
|
433
|
+
d.setdefault("non_interactive", False)
|
|
434
|
+
|
|
435
|
+
# Run rules: a schema field wins over a constraint string, and the two must agree when both are given. The
|
|
436
|
+
# defaults are written into the intake; rule_sources records where each rule came from (see rule_values).
|
|
437
|
+
rules, errs = constraint_rules(d["constraints"])
|
|
438
|
+
errors += errs
|
|
439
|
+
resolved = rule_values(d)
|
|
440
|
+
preference = q.get("preference")
|
|
441
|
+
if preference is not None:
|
|
442
|
+
expected = "lossless" if preference == "exact" else "lossy"
|
|
443
|
+
if resolved["fidelity"]["value"] != expected:
|
|
444
|
+
errors.append(f"quality.preference {preference!r} requires fidelity {expected!r}; "
|
|
445
|
+
"resolve the conflicting field or constraint")
|
|
446
|
+
if preference == "exact":
|
|
447
|
+
warnings.append("quality.preference exact requires equality of the declared deterministic/seeded "
|
|
448
|
+
"outputs; legacy lossless distribution checks alone do not prove identical outputs")
|
|
449
|
+
interaction = d.get("interaction", {})
|
|
450
|
+
if d["non_interactive"] and interaction.get("checkpoints", "required") != "required":
|
|
451
|
+
errors.append("non_interactive: true cannot promise human quality or milestone checkpoints")
|
|
452
|
+
for name, where in (("fidelity", "fidelity"), ("after_target", "goals.after_target")):
|
|
453
|
+
given = resolved[name]["value"]
|
|
454
|
+
if resolved[name]["source"] == where and name in rules and rules[name] != given:
|
|
455
|
+
errors.append(f"{where} is {given!r} but constraints say '{name}: {rules[name]}': keep one of them")
|
|
456
|
+
after = goals["after_target"] = resolved["after_target"]["value"]
|
|
457
|
+
fidelity = d["fidelity"] = resolved["fidelity"]["value"]
|
|
458
|
+
d["rule_sources"] = {name: r["source"] for name, r in resolved.items()}
|
|
459
|
+
if after == "keep_climbing" and hour_cap(d)["host_hours_cap"] is None and not budget.get("deadline"):
|
|
460
|
+
warnings.append("goals.after_target is keep_climbing but no budget cap is set"
|
|
461
|
+
+ (" (budget.usd needs hardware.price_usd_h to become one)" if budget.get("usd") else "")
|
|
462
|
+
+ ": the run climbs until you stop it")
|
|
463
|
+
qbudget = q.get("budget")
|
|
464
|
+
relative = (qbudget or {}).get("relative")
|
|
465
|
+
acceptance = q.get("acceptance")
|
|
466
|
+
if relative is not None:
|
|
467
|
+
if fidelity != "lossy":
|
|
468
|
+
errors.append("quality.budget.relative requires fidelity: lossy")
|
|
469
|
+
if relative["modality"] != modality or modality not in RELATIVE_BUDGET_KEYS:
|
|
470
|
+
errors.append("quality.budget.relative.modality must match the intake modality (image, tts or stt)")
|
|
471
|
+
if not q.get("protocol", "").strip() or not re.fullmatch(r"[0-9a-f]{64}", q.get("protocol_sha256", "")):
|
|
472
|
+
errors.append("quality.budget.relative requires quality.protocol and quality.protocol_sha256 "
|
|
473
|
+
"(64 lowercase hexadecimal characters)")
|
|
474
|
+
allowed = RELATIVE_BUDGET_KEYS[relative["modality"]]
|
|
475
|
+
wrong = sorted(set(relative) - {"modality", *allowed})
|
|
476
|
+
if wrong:
|
|
477
|
+
errors.append(f"quality.budget.relative {wrong} do not apply to modality {relative['modality']}")
|
|
478
|
+
for key, value in relative.items():
|
|
479
|
+
if key != "modality" and isinstance(value, float) and not math.isfinite(value):
|
|
480
|
+
errors.append(f"quality.budget.relative.{key} must be finite")
|
|
481
|
+
if acceptance == PERCEPTUAL_ACCEPTANCE:
|
|
482
|
+
if modality not in PERCEPTUAL_BUDGET_KEYS or relative is None:
|
|
483
|
+
errors.append("quality.acceptance perceptual_and_task_noninferiority requires a typed relative "
|
|
484
|
+
"image or tts budget and hash-bound quality.protocol")
|
|
485
|
+
else:
|
|
486
|
+
missing = sorted(set(PERCEPTUAL_BUDGET_KEYS[modality]) - set(relative))
|
|
487
|
+
if missing:
|
|
488
|
+
errors.append(f"perceptual/task acceptance requires relative bounds {missing}")
|
|
489
|
+
if modality == "image" and ({"lpips_excess_max", "ssim_loss_max"} & set(relative)
|
|
490
|
+
or {"lpips_max", "ssim_min", "psnr_db_min"} & set(qbudget)):
|
|
491
|
+
errors.append("perceptual/task acceptance treats image numerical similarity as diagnostic only; "
|
|
492
|
+
"remove LPIPS/SSIM/PSNR acceptance thresholds")
|
|
493
|
+
wrong = sorted(set(q.get("diagnostic_only", [])) - set(DIAGNOSTIC_QUALITY_METRICS.get(modality, ())))
|
|
494
|
+
if wrong:
|
|
495
|
+
errors.append(f"quality.diagnostic_only {wrong} do not apply to modality {modality}")
|
|
496
|
+
elif "diagnostic_only" in q:
|
|
497
|
+
errors.append("quality.diagnostic_only requires explicit quality.acceptance "
|
|
498
|
+
"perceptual_and_task_noninferiority; it cannot disable existing quality gates")
|
|
499
|
+
if fidelity == "lossless":
|
|
500
|
+
prec, allowed = model.get("precision"), model["precision_allowed"]
|
|
501
|
+
if prec and allowed and allowed != [prec]:
|
|
502
|
+
warnings.append(f"fidelity is lossless but model.precision_allowed {allowed} is not [{prec!r}]: rows at "
|
|
503
|
+
"another precision change numerics; drop them, or set fidelity: lossy with a "
|
|
504
|
+
"quality.budget")
|
|
505
|
+
lossy_kv = [k for k in model["kv_cache_dtype_allowed"] if _LOSSY_KV.match(k)]
|
|
506
|
+
if lossy_kv:
|
|
507
|
+
warnings.append(f"fidelity is lossless but model.kv_cache_dtype_allowed has {lossy_kv}: a low-precision "
|
|
508
|
+
"KV cache changes numerics unless the checkpoint itself specifies it (then write "
|
|
509
|
+
"checkpoint_default); otherwise set fidelity: lossy with a quality.budget")
|
|
510
|
+
if qbudget:
|
|
511
|
+
warnings.append("quality.budget is set but fidelity is lossless: the budget is ignored; set "
|
|
512
|
+
"fidelity: lossy if speed-ups may change numerics")
|
|
513
|
+
elif not qbudget:
|
|
514
|
+
msg = ("fidelity is lossy but quality.budget is missing: declare its bounds "
|
|
515
|
+
f"({', '.join(budget_keys(modality))}) before any measurement")
|
|
516
|
+
if d["non_interactive"]:
|
|
517
|
+
errors.append(msg)
|
|
518
|
+
else:
|
|
519
|
+
warnings.append(msg + "; the agent agrees them with you before ranking a lossy change")
|
|
520
|
+
elif _unbounded(qbudget, modality, acceptance):
|
|
521
|
+
warnings.append(f"fidelity is lossy but quality.budget does not bound {_unbounded(qbudget, modality, acceptance)}: those "
|
|
522
|
+
"metrics have no limit; agree them (add them to quality.budget) before ranking a lossy change")
|
|
523
|
+
other = sorted(set(qbudget or {}) - set(budget_keys(modality)) - {"relative"})
|
|
524
|
+
if fidelity == "lossy" and other:
|
|
525
|
+
warnings.append(f"quality.budget {other} do not apply to modality {modality} (use "
|
|
526
|
+
f"{list(budget_keys(modality))}); they bound nothing")
|
|
527
|
+
d.setdefault("harness", {})
|
|
528
|
+
if access.get("kind") == "ssh" and not d["harness"].get("wheel") and not d["harness"].get("requirements"):
|
|
529
|
+
warnings.append("harness: no wheel or requirements given for the ssh host: install the same inferencekit "
|
|
530
|
+
"version there as here, and record its sha256")
|
|
531
|
+
cands = d.setdefault("candidates", {})
|
|
532
|
+
cands.setdefault("include", [])
|
|
533
|
+
cands.setdefault("exclude", [])
|
|
534
|
+
cands.setdefault("max_engines", 5)
|
|
535
|
+
if modality in ("tts", "stt"):
|
|
536
|
+
warnings.append(f"modality {modality}: bench serve times OpenAI-compatible text endpoints; audio "
|
|
537
|
+
"latencies need a modality client, and claims without one are UNPROVEN")
|
|
538
|
+
if modality == "image":
|
|
539
|
+
warnings.append("modality image: bench serve token metrics and bench clip video metrics do not prove image "
|
|
540
|
+
"latency or quality; a client-timed image adapter and modality quality protocol are required; "
|
|
541
|
+
"unsupported checks are NOT RUN, and claims without them are UNPROVEN")
|
|
542
|
+
if modality == "video":
|
|
543
|
+
warnings.append("modality video: time clips with bench clip (clip HTTP contract or ComfyUI) and check them "
|
|
544
|
+
"with gates clip and quality clip; bench serve's token metrics and buckets do not apply")
|
|
545
|
+
d["intake_version"] = 1
|
|
546
|
+
if not d.get("run_id"):
|
|
547
|
+
stamp = _dt.datetime.now(_dt.timezone.utc).strftime("%Y%m%d-%H%M")
|
|
548
|
+
d["run_id"] = f"{_slug(model['id'].split('/')[-1])}-{_slug(hw['gpu'])}-{stamp}"
|
|
549
|
+
return d, errors, warnings
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# --------------------------------------------------------------------------- the derived plan
|
|
553
|
+
|
|
554
|
+
def hour_cap(intake: dict) -> dict:
|
|
555
|
+
"""The binding cap on host hours: the smallest of gpu_hours/count, usd/price, wall_clock_h."""
|
|
556
|
+
b, hw = intake["budget"], intake["hardware"]
|
|
557
|
+
caps = {}
|
|
558
|
+
if b.get("gpu_hours"):
|
|
559
|
+
caps["gpu_hours"] = b["gpu_hours"] / hw["count"]
|
|
560
|
+
if b.get("usd") and hw.get("price_usd_h"):
|
|
561
|
+
caps["usd"] = b["usd"] / hw["price_usd_h"]
|
|
562
|
+
if b.get("wall_clock_h"):
|
|
563
|
+
caps["wall_clock_h"] = b["wall_clock_h"]
|
|
564
|
+
if not caps:
|
|
565
|
+
return {"host_hours_cap": None, "binding": None, "caps_h": {}, "usd_cap": b.get("usd"),
|
|
566
|
+
"formula": "no cap given", "spend_cap": b.get("spend_cap", "bounded"),
|
|
567
|
+
"deadline": b.get("deadline")}
|
|
568
|
+
binding = min(caps, key=caps.get)
|
|
569
|
+
usd_cap = b.get("usd")
|
|
570
|
+
if usd_cap is None and b.get("spend_cap") != "none" and hw.get("price_usd_h"):
|
|
571
|
+
usd_cap = round(caps[binding] * hw["price_usd_h"], 2)
|
|
572
|
+
return {"host_hours_cap": round(caps[binding], 3), "binding": binding,
|
|
573
|
+
"caps_h": {k: round(v, 3) for k, v in caps.items()}, "usd_cap": usd_cap,
|
|
574
|
+
"formula": "min(gpu_hours / count, usd / price_usd_h, wall_clock_h)",
|
|
575
|
+
"spend_cap": b.get("spend_cap", "bounded"), "deadline": b.get("deadline")}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def rule_values(intake: dict) -> dict:
|
|
579
|
+
"""{name: {"value", "source"}} for every run rule in RULES: its schema field when written, else a constraint
|
|
580
|
+
string, else the default. ``source`` is the field name, ``constraints`` or ``default: ...``.
|
|
581
|
+
|
|
582
|
+
normalize writes the defaults into the intake and records each source in ``rule_sources``. A field value that
|
|
583
|
+
``rule_sources`` says came from a constraint or a default, and still equals what that would give, keeps that
|
|
584
|
+
source, so checking a normalized intake again gives the same plan.
|
|
585
|
+
"""
|
|
586
|
+
found, _ = constraint_rules(intake.get("constraints"))
|
|
587
|
+
goals = intake.get("goals") if isinstance(intake.get("goals"), dict) else {}
|
|
588
|
+
budget = intake.get("budget") if isinstance(intake.get("budget"), dict) else {}
|
|
589
|
+
prior = intake.get("rule_sources") if isinstance(intake.get("rule_sources"), dict) else {}
|
|
590
|
+
lead = goals.get("lead", "single_user")
|
|
591
|
+
if intake.get("non_interactive"):
|
|
592
|
+
after_default = ("keep_climbing", "default: non_interactive")
|
|
593
|
+
elif budget.get("deadline"):
|
|
594
|
+
after_default = ("keep_climbing", "default: budget.deadline is set")
|
|
595
|
+
elif any(budget.get(k) for k in CAP_KEYS):
|
|
596
|
+
after_default = ("keep_climbing", "default: budget.gpu_hours, usd or wall_clock_h is set")
|
|
597
|
+
else:
|
|
598
|
+
after_default = ("stop", "default: interactive, no budget cap")
|
|
599
|
+
preference = (intake.get("quality") or {}).get("preference")
|
|
600
|
+
fidelity_default = (("lossless" if preference == "exact" else "lossy", "quality.preference")
|
|
601
|
+
if preference is not None else ("lossless", "default"))
|
|
602
|
+
rules = {"after_target": ("goals.after_target", goals.get("after_target"), after_default),
|
|
603
|
+
"fidelity": ("fidelity", intake.get("fidelity"), fidelity_default),
|
|
604
|
+
"speculation": (None, None, ("auto", "default")),
|
|
605
|
+
"route": (None, None, ("build" if lead == "single_user" else "patch", f"default: goals.lead {lead}"))}
|
|
606
|
+
out = {}
|
|
607
|
+
for name, (field, written, (default, default_source)) in rules.items():
|
|
608
|
+
fallback = ({"value": found[name], "source": "constraints"} if name in found
|
|
609
|
+
else {"value": default, "source": default_source})
|
|
610
|
+
filled_in = prior.get(name, field) != field and written == fallback["value"]
|
|
611
|
+
out[name] = {"value": written, "source": field} if written is not None and not filled_in else fallback
|
|
612
|
+
return out
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _stop_rule(named: bool, after: str, ni: bool, capped: bool, explicit: bool, set_keys: list[str],
|
|
616
|
+
where: str = "in every listed bucket") -> dict:
|
|
617
|
+
"""``capped``: the budget gives an hour cap (hour_cap), so a climbing run has an end. ``explicit``: after_target
|
|
618
|
+
was written (field or constraint), not defaulted. ``set_keys``: the budget keys that made keep_climbing the
|
|
619
|
+
default. ``where``: where a target must hold (video: over the whole clip set). Without a target, an
|
|
620
|
+
interactive run still asks at the stop point unless the person wrote keep_climbing."""
|
|
621
|
+
until = "until the budget ends" if capped else "until you stop it (no budget cap is set)"
|
|
622
|
+
if named and after == "keep_climbing":
|
|
623
|
+
return {"kind": "target", "text": f"keep climbing {until}; record the best result over time (screened or verified). The "
|
|
624
|
+
f"targets are milestones: record when each is first met by its conservative CI bound {where}, "
|
|
625
|
+
"then continue (the report reserve is kept)"}
|
|
626
|
+
if named:
|
|
627
|
+
return {"kind": "target", "text": f"stop when every target is met by its conservative CI bound {where}; "
|
|
628
|
+
"when one is missed, continue to stage 7 within the approved budget"}
|
|
629
|
+
if after == "keep_climbing" and (ni or explicit):
|
|
630
|
+
return {"kind": "budget", "text": f"no target: keep climbing {until}, stage 7 included, keeping the report "
|
|
631
|
+
"reserve; record the best result over time (screened or verified)"}
|
|
632
|
+
if ni:
|
|
633
|
+
return {"kind": "stop_point", "text": "no target and after_target stop: stop after the stage 6 ranking and "
|
|
634
|
+
"write the report (stage 7 NOT RUN)"}
|
|
635
|
+
text = "no target: show the ranked table at stage 6 and ask whether to stop or continue"
|
|
636
|
+
if after == "keep_climbing":
|
|
637
|
+
text += (f"; the default to offer is keep climbing {until}, recording the best result over time (screened or verified) "
|
|
638
|
+
f"(after_target defaults to keep_climbing because the intake sets "
|
|
639
|
+
f"{', '.join('budget.' + k for k in set_keys)})")
|
|
640
|
+
return {"kind": "ask", "text": text}
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def budget_keys(modality: str) -> tuple[str, ...]:
|
|
644
|
+
"""The quality-budget bounds applicable to a modality."""
|
|
645
|
+
if modality in ("image", "video"):
|
|
646
|
+
return VIDEO_BUDGET_KEYS
|
|
647
|
+
if modality in ("tts", "stt"):
|
|
648
|
+
return SPEECH_BUDGET_KEYS
|
|
649
|
+
return QUALITY_BUDGET_KEYS
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _unbounded(qbudget, modality: str = "llm", acceptance: str | None = None) -> list[str]:
|
|
653
|
+
"""The quality-budget bounds a lossy run leaves out."""
|
|
654
|
+
if (qbudget or {}).get("relative"):
|
|
655
|
+
expected = PERCEPTUAL_BUDGET_KEYS if acceptance == PERCEPTUAL_ACCEPTANCE else RELATIVE_BUDGET_KEYS
|
|
656
|
+
return ["relative." + k for k in expected.get(modality, ())
|
|
657
|
+
if k not in qbudget["relative"]]
|
|
658
|
+
return [k for k in budget_keys(modality) if k not in (qbudget or {})]
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _fidelity_text(fidelity: str, qbudget, modality: str = "llm") -> str:
|
|
662
|
+
if modality in ("image", "tts", "stt"):
|
|
663
|
+
return (f"{fidelity}: validate the exact speed configuration using quality.protocol and the declared "
|
|
664
|
+
"modality quality budget against the reference and A/A noise floor; no LLM token/GSM8K proxy. "
|
|
665
|
+
"Numerics changes are declared; missing modality evidence is UNPROVEN")
|
|
666
|
+
if fidelity == "lossless" and modality == "video":
|
|
667
|
+
return ("lossless: numerics stay as shipped (the checkpoint's own precision, the same steps, shape, guidance "
|
|
668
|
+
"and initial noise). Only changes that leave the clips unchanged count: refactors, equivalent "
|
|
669
|
+
"kernels, compilation, overlapped stages. Proof: quality clip (PSNR, SSIM, LPIPS per frame against "
|
|
670
|
+
"the BF16 reference, within the run-to-run and alternative-backend floors); without it a change is "
|
|
671
|
+
"UNPROVEN")
|
|
672
|
+
if fidelity == "lossless":
|
|
673
|
+
return ("lossless: numerics stay as shipped (weights and activations in the checkpoint's own precision; no "
|
|
674
|
+
"KV cache below 16 bits unless the checkpoint ships one). Only changes that leave the output "
|
|
675
|
+
"distribution unchanged count: refactors, equivalent kernels, re-encodings that decode to identical "
|
|
676
|
+
"values, exact speculative decoding. Proof: next-token agreement and KL against the reference within "
|
|
677
|
+
"the noise floor; without it a change is UNPROVEN")
|
|
678
|
+
text = ("lossy: changes that alter numerics are allowed when every bound in quality_budget holds against the "
|
|
679
|
+
"reference; they are a separate track, reported as speed at a stated quality cost and never ranked "
|
|
680
|
+
"against lossless results")
|
|
681
|
+
if not qbudget:
|
|
682
|
+
return text + ". quality_budget is not set yet: agree it before ranking a lossy change"
|
|
683
|
+
missing = _unbounded(qbudget, modality)
|
|
684
|
+
return text + (f". Not bounded: {missing}; agree them before ranking a lossy change" if missing else "")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _precision_rule(model: dict, fidelity: str, modality: str = "llm") -> str:
|
|
688
|
+
"""Which numerics a ranked engine may use. Under lossless only the as-shipped ones: model.precision (when given)
|
|
689
|
+
and KV dtypes of 16 bits or more; the other listed entries are named as levers that are not run or ranked.
|
|
690
|
+
A video model has no KV cache: only the precision part applies."""
|
|
691
|
+
allowed, kv = model["precision_allowed"], model["kv_cache_dtype_allowed"]
|
|
692
|
+
if modality in ("image", "video"):
|
|
693
|
+
kv = []
|
|
694
|
+
prec, kv_ranked = allowed, kv
|
|
695
|
+
if fidelity == "lossless":
|
|
696
|
+
prec = [model["precision"]] if model.get("precision") else allowed
|
|
697
|
+
kv_ranked = [k for k in kv if not _LOSSY_KV.match(k)] or ([] if modality in ("image", "video") else ["checkpoint_default"])
|
|
698
|
+
text = (f"every ranked engine runs weights/activations in one of {prec}"
|
|
699
|
+
+ (f" and KV cache in one of {kv_ranked}" if modality not in ("image", "video") else "")
|
|
700
|
+
+ "; different precisions are separate tracks")
|
|
701
|
+
extra = [f"{what} {left}" for what, left in (("precision", [p for p in allowed if p not in prec]),
|
|
702
|
+
("KV cache", [k for k in kv if k not in kv_ranked])) if left]
|
|
703
|
+
if extra:
|
|
704
|
+
text += (f". Listed but outside fidelity lossless (they change numerics): {', '.join(extra)}; they are "
|
|
705
|
+
"reported as levers, not run or ranked, unless fidelity is lossy with a quality.budget")
|
|
706
|
+
return text
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _speculation_text(value: str, fidelity: str, modality: str = "llm") -> str:
|
|
710
|
+
if modality in ("image", "video"):
|
|
711
|
+
return f"does not apply to modality {modality} (no token decoding)"
|
|
712
|
+
if value == "off":
|
|
713
|
+
return "off: no speculative decoding in any ranked engine (no draft model, MTP head or n-gram lookup)"
|
|
714
|
+
return ("auto: speculative decoding is an ordinary lever where it pays (single user, low concurrency), kept only "
|
|
715
|
+
"if faster on this workload; " + ("only exact speculation counts: greedy output identical and the "
|
|
716
|
+
"sampling distribution unchanged" if fidelity == "lossless" else
|
|
717
|
+
"inexact acceptance counts only within quality_budget"))
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
ROUTE_TEXT = {"build": "build: write a single-purpose engine for this model and workload; the best tuned existing "
|
|
721
|
+
"engine stays the baseline",
|
|
722
|
+
"patch": "patch: tune and patch the best existing engine"}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def derive(intake: dict) -> dict:
|
|
726
|
+
"""The derived plan (claim class ``derived``) of a normalized intake. Where each run rule came from is read
|
|
727
|
+
from the intake's ``rule_sources`` (see rule_values), so deriving twice gives the same plan."""
|
|
728
|
+
goals, targets = intake["goals"], intake["goals"]["targets"]
|
|
729
|
+
named = [k for k in targets if k != "buckets"]
|
|
730
|
+
video = intake["modality"] in ("image", "video")
|
|
731
|
+
target_rows = [{"name": k, "value": targets[k], "better": TARGETS[k][0], "unit": TARGETS[k][1],
|
|
732
|
+
"buckets": ["all"] if video else targets.get("buckets", intake["buckets"]),
|
|
733
|
+
"applies": k in MODALITY_TARGETS[intake["modality"]]} for k in named]
|
|
734
|
+
ni = intake["non_interactive"]
|
|
735
|
+
rules = rule_values(intake)
|
|
736
|
+
after = rules["after_target"]["value"]
|
|
737
|
+
after_source = rules["after_target"]["source"]
|
|
738
|
+
cap = hour_cap(intake)
|
|
739
|
+
hours = cap["host_hours_cap"]
|
|
740
|
+
set_keys = [k for k in CAP_KEYS if intake["budget"].get(k)]
|
|
741
|
+
target_scope = ("over the whole clip set" if intake["modality"] == "video" else "over the whole image set") if video else "in every listed bucket"
|
|
742
|
+
stop = {**_stop_rule(bool(named), after, ni, hours is not None or bool(intake["budget"].get("deadline")),
|
|
743
|
+
not after_source.startswith("default"), set_keys, target_scope),
|
|
744
|
+
"after_target": after, "after_target_source": after_source}
|
|
745
|
+
if intake["budget"].get("report_reserve_h") == 0:
|
|
746
|
+
stop["text"] = stop["text"].replace("the report reserve is kept", "no early reserve; validate/report continuously")
|
|
747
|
+
stop["text"] = stop["text"].replace("keeping the report reserve", "validating/reporting continuously")
|
|
748
|
+
interaction = intake.get("interaction") or {}
|
|
749
|
+
if interaction.get("checkpoints") == "required" and not named and stop["kind"] == "ask":
|
|
750
|
+
if after == "keep_climbing" and (hours is not None or intake["budget"].get("deadline")):
|
|
751
|
+
stop.update(kind="budget", text="no target: continue within the authorized budget; pause only "
|
|
752
|
+
"for a new required decision")
|
|
753
|
+
else:
|
|
754
|
+
stop.update(kind="stop_point", text="no target: finish the ranking and report; further scope "
|
|
755
|
+
"needs a new decision")
|
|
756
|
+
fidelity = rules["fidelity"]["value"]
|
|
757
|
+
qbudget = (intake.get("quality") or {}).get("budget") if fidelity == "lossy" else None
|
|
758
|
+
boxes = {k: (round(hours * share, 3) if hours and intake["budget"].get("stage_timeboxes", True) else None)
|
|
759
|
+
for k, share in STAGES}
|
|
760
|
+
modes = ["single"]
|
|
761
|
+
if goals["lead"] == "capacity" or "n_star" in targets:
|
|
762
|
+
modes.append("load")
|
|
763
|
+
if intake["modality"] == "image":
|
|
764
|
+
modes = ["image"] # requires an external image adapter; no built-in command implied
|
|
765
|
+
if intake["modality"] == "video":
|
|
766
|
+
modes = ["clip"] # bench rounds --mode clip: one clip at a time
|
|
767
|
+
questions = []
|
|
768
|
+
if ni:
|
|
769
|
+
if not named:
|
|
770
|
+
questions.append({"question": "No target was given. Stop after stage 6 or continue?",
|
|
771
|
+
"choice": ("keep climbing while budget remains (report reserve kept)"
|
|
772
|
+
if after == "keep_climbing" else "stop after stage 6 (after_target: stop)")})
|
|
773
|
+
elif rules["after_target"]["source"].startswith("default"):
|
|
774
|
+
questions.append({"question": "Once every target is met: stop, or keep climbing?",
|
|
775
|
+
"choice": f"{after} ({rules['after_target']['source']})"})
|
|
776
|
+
if rules["fidelity"]["source"] == "default":
|
|
777
|
+
questions.append({"question": "May speed-ups change numerics (lossy), and inside which quality budget?",
|
|
778
|
+
"choice": "no: fidelity lossless (the default), numerics stay as shipped"})
|
|
779
|
+
if rules["route"]["source"].startswith("default"):
|
|
780
|
+
questions.append({"question": "Build a new engine, or patch the best existing one?",
|
|
781
|
+
"choice": f"{rules['route']['value']} ({rules['route']['source']})"})
|
|
782
|
+
if intake["repo"] and intake["repo"]["push"] == "ask":
|
|
783
|
+
questions.append({"question": "Push the perf branch?", "choice": "no push (non_interactive never "
|
|
784
|
+
"pushes)"})
|
|
785
|
+
if intake["quality"]["reference"]["kind"] == "none":
|
|
786
|
+
questions.append({"question": "Which reference should outputs be compared with?",
|
|
787
|
+
"choice": "the baseline engine at the checkpoint's precision"})
|
|
788
|
+
if not intake["model"].get("revision"):
|
|
789
|
+
questions.append({"question": "Which model revision?", "choice": "the commit sha current at stage 1"})
|
|
790
|
+
return {
|
|
791
|
+
"claim_class": "derived",
|
|
792
|
+
"run_id": intake["run_id"],
|
|
793
|
+
"modes": modes,
|
|
794
|
+
"stop_rule": stop,
|
|
795
|
+
"targets": target_rows,
|
|
796
|
+
"budget": cap,
|
|
797
|
+
"time_boxes_h": boxes,
|
|
798
|
+
"report_reserve_h": intake["budget"].get("report_reserve_h",
|
|
799
|
+
round(hours * REPORT_RESERVE, 3) if hours else None),
|
|
800
|
+
"stage_timeboxes": intake["budget"].get("stage_timeboxes", True),
|
|
801
|
+
"affordability": goals.get("affordability"),
|
|
802
|
+
"precision_rule": _precision_rule(intake["model"], fidelity, intake["modality"]),
|
|
803
|
+
"fidelity_rule": {"value": fidelity, "fidelity": fidelity, "source": rules["fidelity"]["source"],
|
|
804
|
+
"text": ("exact: equality of the declared deterministic/seeded outputs is required; "
|
|
805
|
+
"legacy lossless distribution checks alone are insufficient"
|
|
806
|
+
if intake["quality"].get("preference") == "exact"
|
|
807
|
+
else _fidelity_text(fidelity, qbudget, intake["modality"]))},
|
|
808
|
+
"quality_budget": qbudget or None,
|
|
809
|
+
"quality_preference": intake["quality"].get("preference"),
|
|
810
|
+
"architecture_changes": intake["model"].get("architecture_changes", "forbid"),
|
|
811
|
+
"interaction": {"checkpoints": interaction.get("checkpoints", "required"),
|
|
812
|
+
"show_plan": interaction.get("show_plan", False)},
|
|
813
|
+
**({"external_quality_proof": {
|
|
814
|
+
"required": True, "quality_verdict": "UNPROVEN",
|
|
815
|
+
"protocol": intake["quality"]["protocol"],
|
|
816
|
+
"protocol_sha256": intake["quality"]["protocol_sha256"],
|
|
817
|
+
"budget_basis": "relative degradation; positive increase/drop is worse",
|
|
818
|
+
"text": "External protocol quality proof required on the exact speed configuration, including "
|
|
819
|
+
"its reference/A/A, cohort and CI rules. Intake validation is not a quality PASS; "
|
|
820
|
+
"no absolute scorer or bitwise output test evaluates this relative budget.",
|
|
821
|
+
**({"acceptance": PERCEPTUAL_ACCEPTANCE,
|
|
822
|
+
"diagnostic_only": intake["quality"].get("diagnostic_only", []),
|
|
823
|
+
"blinded_perceptual_review_required": True,
|
|
824
|
+
"perceptual_verdict": "UNPROVEN",
|
|
825
|
+
"text": "External blinded perceptual and task-quality proof required on the exact speed "
|
|
826
|
+
"configuration under the hash-bound protocol. Numerical similarity is diagnostic, "
|
|
827
|
+
"not an independent rejection gate. Proxy metrics alone do not establish perceptual "
|
|
828
|
+
"quality; missing viewing/listening stays UNPROVEN. Intake validation is not quality PASS."}
|
|
829
|
+
if intake["quality"].get("acceptance") == PERCEPTUAL_ACCEPTANCE else {}),
|
|
830
|
+
}} if qbudget and "relative" in qbudget else {}),
|
|
831
|
+
"speculation": {**rules["speculation"], "text": _speculation_text(rules["speculation"]["value"], fidelity,
|
|
832
|
+
intake["modality"])},
|
|
833
|
+
"route": {**rules["route"], "text": ROUTE_TEXT[rules["route"]["value"]]},
|
|
834
|
+
"questions": questions,
|
|
835
|
+
"chat_template_kwargs": (intake.get("workload") or {}).get("chat_template_kwargs") or None,
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _drop_nulls(node, path=()):
|
|
840
|
+
"""Legacy empty YAML blocks are absent; explicit typed quality declarations must validate."""
|
|
841
|
+
if isinstance(node, dict):
|
|
842
|
+
return {k: _drop_nulls(v, path + (k,)) for k, v in node.items()
|
|
843
|
+
if v is not None or path == ("quality", "budget", "relative")
|
|
844
|
+
or (path == ("quality", "budget") and k == "relative")
|
|
845
|
+
or (path == ("quality",) and k in ("acceptance", "diagnostic_only"))}
|
|
846
|
+
if isinstance(node, list):
|
|
847
|
+
return [_drop_nulls(v, path + (i,)) for i, v in enumerate(node)]
|
|
848
|
+
return node
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def check(data: dict, allow_placeholders: bool = False) -> dict:
|
|
852
|
+
"""Validate a parsed intake. Raises OpError listing every error; returns the normalized result."""
|
|
853
|
+
data = _drop_nulls(data)
|
|
854
|
+
errors, warnings = schema_problems(data)
|
|
855
|
+
if errors:
|
|
856
|
+
raise OpError("intake is invalid:\n - " + "\n - ".join(errors + [f"(warning) {w}" for w in warnings]))
|
|
857
|
+
data = _drop_unknown(data, load_schema())
|
|
858
|
+
holes = placeholders(data)
|
|
859
|
+
intake, errs, warns = normalize(data)
|
|
860
|
+
errors += errs
|
|
861
|
+
warnings += warns
|
|
862
|
+
if holes:
|
|
863
|
+
msg = f"placeholder values still present at {holes}: replace them before a run"
|
|
864
|
+
(warnings if allow_placeholders else errors).append(msg)
|
|
865
|
+
if errors:
|
|
866
|
+
raise OpError("intake is invalid:\n - " + "\n - ".join(errors + [f"(warning) {w}" for w in warnings]))
|
|
867
|
+
return {"ok": True, "intake": intake, "warnings": warnings, "placeholders": holes, "derived": derive(intake)}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
# --------------------------------------------------------------------------- run directory
|
|
871
|
+
|
|
872
|
+
def initial_state(result: dict, intake_sha: str) -> dict:
|
|
873
|
+
now = utc_now()
|
|
874
|
+
derived = result["derived"]
|
|
875
|
+
stages = {k: {"status": "pending", "started": None, "ended": None, "time_box_h": derived["time_boxes_h"][k],
|
|
876
|
+
"evidence": [], "notes": []} for k, _ in STAGES}
|
|
877
|
+
stages["1_intake"].update(status="in_progress", started=now)
|
|
878
|
+
state = {
|
|
879
|
+
"state_version": 1,
|
|
880
|
+
"run_id": derived["run_id"],
|
|
881
|
+
"intake_sha256": intake_sha,
|
|
882
|
+
"created": now,
|
|
883
|
+
"updated": now,
|
|
884
|
+
"current_stage": "1_intake",
|
|
885
|
+
"stages": stages,
|
|
886
|
+
"budget": {"host_hours_cap": derived["budget"]["host_hours_cap"], "usd_cap": derived["budget"]["usd_cap"],
|
|
887
|
+
"price_usd_h": result["intake"]["hardware"].get("price_usd_h"), "run_started": now,
|
|
888
|
+
"meter": [], "spend_cap": derived["budget"].get("spend_cap", "bounded"),
|
|
889
|
+
"deadline": derived["budget"].get("deadline"),
|
|
890
|
+
"stage_timeboxes": derived["stage_timeboxes"],
|
|
891
|
+
"report_reserve_h": derived["report_reserve_h"]},
|
|
892
|
+
"model": {"id": result["intake"]["model"]["id"], "revision": result["intake"]["model"].get("revision"),
|
|
893
|
+
"revision_resolved": None},
|
|
894
|
+
"candidates": {},
|
|
895
|
+
"best": None,
|
|
896
|
+
"decisions": [],
|
|
897
|
+
"next_action": "stage 1: export INFERENCEKIT_RUNS_DIR=<abs path>/runs/<run-id>/evidence (here and on the GPU "
|
|
898
|
+
"host), resolve the model revision to a commit sha, write QUESTIONS.md, then stage 2",
|
|
899
|
+
}
|
|
900
|
+
pin = (result["intake"].get("workload") or {}).get("chat_template_kwargs")
|
|
901
|
+
if pin:
|
|
902
|
+
text = json.dumps(pin, sort_keys=True, separators=(",", ":"))
|
|
903
|
+
state["workload_pin"] = {"chat_template_kwargs": pin}
|
|
904
|
+
state["next_action"] += (f"; the intake pins chat_template_kwargs {text}: put it in manifest.json "
|
|
905
|
+
"workload.chat_template_kwargs and pass --chat-template-kwargs '" + text + "' to "
|
|
906
|
+
"bench serve, gates run and quality (with --run-dir runs/<run-id> they apply it and "
|
|
907
|
+
"refuse a different value)")
|
|
908
|
+
return state
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
# --------------------------------------------------------------------------- the workload pin
|
|
912
|
+
|
|
913
|
+
PIN_FILE = "intake.normalized.json"
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def intake_pin(run_dir: str | os.PathLike | None) -> dict:
|
|
917
|
+
"""The intake's ``workload.chat_template_kwargs`` recorded in ``<run_dir>/intake.normalized.json``
|
|
918
|
+
({} when there is no run dir, no normalized intake or no pin). The pin is binding: every command that
|
|
919
|
+
generates with that run dir sends it (see apply_pin), and ``manifest validate`` / ``bench rounds`` refuse
|
|
920
|
+
a manifest that contradicts it."""
|
|
921
|
+
if not run_dir:
|
|
922
|
+
return {}
|
|
923
|
+
path = Path(run_dir) / PIN_FILE
|
|
924
|
+
try:
|
|
925
|
+
norm = json.loads(path.read_text())
|
|
926
|
+
except (OSError, ValueError):
|
|
927
|
+
return {}
|
|
928
|
+
pin = (((norm.get("intake") or {}).get("workload") or {}).get("chat_template_kwargs")
|
|
929
|
+
if isinstance(norm, dict) else None)
|
|
930
|
+
return dict(pin) if isinstance(pin, dict) else {}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def pin_conflicts(value: dict | None, pin: dict) -> list[str]:
|
|
934
|
+
"""Pinned keys that ``value`` sets to something else (a missing key is not a conflict)."""
|
|
935
|
+
value = value or {}
|
|
936
|
+
return [f"{k}: {json.dumps(value[k])} (pinned {json.dumps(v)})" for k, v in sorted(pin.items())
|
|
937
|
+
if k in value and value[k] != v]
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def apply_pin(value: dict | None, run_dir, where: str) -> dict:
|
|
941
|
+
"""``value`` with the run dir's intake pin applied: pinned keys are added when missing; a different value
|
|
942
|
+
for a pinned key is refused (OpError naming ``where``)."""
|
|
943
|
+
pin = intake_pin(run_dir)
|
|
944
|
+
if not pin:
|
|
945
|
+
return dict(value or {})
|
|
946
|
+
bad = pin_conflicts(value, pin)
|
|
947
|
+
if bad:
|
|
948
|
+
raise OpError(f"{where}: chat_template_kwargs contradicts the intake's pinned "
|
|
949
|
+
f"workload.chat_template_kwargs in {Path(run_dir) / PIN_FILE}: {'; '.join(bad)}. The intake "
|
|
950
|
+
"is pre-registered: every engine runs with the pinned template settings")
|
|
951
|
+
return {**(value or {}), **pin}
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _home() -> str | None:
|
|
955
|
+
home = os.path.expanduser("~")
|
|
956
|
+
return home if home not in ("", "~", os.sep) else None
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def display_path(path: str | os.PathLike) -> str:
|
|
960
|
+
"""A path for artifacts: relative to the working directory when under it, else with the home directory as ~."""
|
|
961
|
+
p = Path(path).resolve()
|
|
962
|
+
try:
|
|
963
|
+
return str(p.relative_to(Path.cwd().resolve()))
|
|
964
|
+
except ValueError:
|
|
965
|
+
pass
|
|
966
|
+
home = _home()
|
|
967
|
+
s = str(p)
|
|
968
|
+
if home and (s == home or s.startswith(home.rstrip(os.sep) + os.sep)):
|
|
969
|
+
return "~" + s[len(home.rstrip(os.sep)):]
|
|
970
|
+
return s
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def redact(intake: dict) -> dict:
|
|
974
|
+
"""A copy of an intake that is safe to keep in the run directory (which is synced, reviewed and published).
|
|
975
|
+
|
|
976
|
+
* every ``hardware.access`` field except ``kind`` becomes ``<redacted>`` (host, user, port, key_path);
|
|
977
|
+
* the host anywhere else (e.g. inside ``quality.reference.base_url``) and ``<user>@`` become ``<redacted>``;
|
|
978
|
+
* the home directory at the start of a path becomes ``~`` (it names the local user).
|
|
979
|
+
"""
|
|
980
|
+
d = json.loads(json.dumps(intake, default=str))
|
|
981
|
+
access = (d.get("hardware") or {}).get("access") if isinstance(d.get("hardware"), dict) else None
|
|
982
|
+
host = user = None
|
|
983
|
+
if isinstance(access, dict):
|
|
984
|
+
host = access.get("host") if isinstance(access.get("host"), str) and access.get("host") != REDACTED else None
|
|
985
|
+
user = access.get("user") if isinstance(access.get("user"), str) and access.get("user") != REDACTED else None
|
|
986
|
+
host_re = re.compile(r"(?<![\w.-])" + re.escape(host) + r"(?![\w-])") if host else None
|
|
987
|
+
user_re = re.compile(r"(?<![\w.-])" + re.escape(user) + r"@") if user else None
|
|
988
|
+
home = _home()
|
|
989
|
+
home_prefix = home.rstrip(os.sep) + os.sep if home else None
|
|
990
|
+
|
|
991
|
+
def walk(node):
|
|
992
|
+
if isinstance(node, dict):
|
|
993
|
+
return {k: walk(v) for k, v in node.items()}
|
|
994
|
+
if isinstance(node, list):
|
|
995
|
+
return [walk(v) for v in node]
|
|
996
|
+
if isinstance(node, str):
|
|
997
|
+
if user_re:
|
|
998
|
+
node = user_re.sub(REDACTED + "@", node)
|
|
999
|
+
if host_re:
|
|
1000
|
+
node = host_re.sub(REDACTED, node)
|
|
1001
|
+
if home_prefix and node.startswith(home_prefix):
|
|
1002
|
+
node = "~" + os.sep + node[len(home_prefix):]
|
|
1003
|
+
return node
|
|
1004
|
+
|
|
1005
|
+
d = walk(d)
|
|
1006
|
+
if isinstance(access, dict):
|
|
1007
|
+
d["hardware"]["access"] = {k: (v if k == "kind" else REDACTED) for k, v in access.items()}
|
|
1008
|
+
return d
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _copy_text(raw: bytes, as_json: bool, sha: str) -> str:
|
|
1012
|
+
"""The redacted copy of the intake file as written into the run directory."""
|
|
1013
|
+
original = redact(parse_text(raw.decode("utf-8")))
|
|
1014
|
+
if as_json:
|
|
1015
|
+
return json.dumps(original, indent=2, ensure_ascii=False) + "\n"
|
|
1016
|
+
import yaml # parse_text already needed it for a YAML intake
|
|
1017
|
+
|
|
1018
|
+
head = ("# Redacted copy of this run's intake: hardware.access details are removed and the home directory\n"
|
|
1019
|
+
f"# is shown as ~. intake_sha256 of the original file: {sha}\n"
|
|
1020
|
+
"# To resume, validate the ORIGINAL intake file (state.json: intake_source), not this copy.\n")
|
|
1021
|
+
return head + yaml.safe_dump(original, sort_keys=False, allow_unicode=True, default_flow_style=False)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _recorded_shas(run_dir: Path) -> dict:
|
|
1025
|
+
"""intake_sha256 values already recorded in the run directory, by file. Raises on unreadable JSON."""
|
|
1026
|
+
out = {}
|
|
1027
|
+
for name in ("state.json", "intake.normalized.json"):
|
|
1028
|
+
p = run_dir / name
|
|
1029
|
+
if p.exists():
|
|
1030
|
+
try:
|
|
1031
|
+
out[name] = json.loads(p.read_text()).get("intake_sha256")
|
|
1032
|
+
except (ValueError, AttributeError):
|
|
1033
|
+
raise OpError(f"{p} is not valid JSON; fix or move it before resuming") from None
|
|
1034
|
+
return out
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def start_run(run_dir: Path, src: Path, raw: bytes, result: dict) -> dict:
|
|
1038
|
+
"""Create or resume runs/<run-id>/.
|
|
1039
|
+
|
|
1040
|
+
The intake is pre-registered: if the run directory already records a different intake_sha256, this raises and
|
|
1041
|
+
writes nothing (a changed intake is a new run id). Existing files are never overwritten: the intake copy,
|
|
1042
|
+
intake.normalized.json, state.json, experiments.jsonl and QUESTIONS.md are only created when missing.
|
|
1043
|
+
Access details (host, user, port, key path) never reach the run directory: the copies are redacted.
|
|
1044
|
+
"""
|
|
1045
|
+
sha = sha256_hex(raw)
|
|
1046
|
+
recorded = _recorded_shas(run_dir)
|
|
1047
|
+
changed = sorted(name for name, old in recorded.items() if old != sha)
|
|
1048
|
+
if changed:
|
|
1049
|
+
raise OpError(f"the intake changed since this run started: {', '.join(changed)} in {run_dir} record(s) "
|
|
1050
|
+
f"intake_sha256 {recorded[changed[0]]!r}, this file is {sha!r}. Targets are pre-registered, "
|
|
1051
|
+
"so a changed intake is a new run: start a new run id (--run-dir runs/<new-run-id>). "
|
|
1052
|
+
"Nothing was written.")
|
|
1053
|
+
resumed = "state.json" in recorded
|
|
1054
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
1055
|
+
written = []
|
|
1056
|
+
as_json = src.suffix.lower() == ".json"
|
|
1057
|
+
copy_name = "intake.json" if as_json else "intake.yaml"
|
|
1058
|
+
dst = run_dir / copy_name
|
|
1059
|
+
if not dst.exists():
|
|
1060
|
+
dst.write_text(_copy_text(raw, as_json, sha))
|
|
1061
|
+
written.append(copy_name)
|
|
1062
|
+
source = display_path(src)
|
|
1063
|
+
norm_path = run_dir / "intake.normalized.json"
|
|
1064
|
+
if not norm_path.exists():
|
|
1065
|
+
norm = {"intake_sha256": sha, "intake_source": source, "validated": utc_now(),
|
|
1066
|
+
**{k: v for k, v in result.items() if k not in ("intake_sha256", "intake")},
|
|
1067
|
+
"intake": redact(result["intake"])}
|
|
1068
|
+
norm_path.write_text(json.dumps(norm, indent=2, ensure_ascii=False) + "\n")
|
|
1069
|
+
written.append("intake.normalized.json")
|
|
1070
|
+
if not resumed:
|
|
1071
|
+
state = initial_state(result, sha)
|
|
1072
|
+
state["intake_source"] = source
|
|
1073
|
+
(run_dir / "state.json").write_text(json.dumps(state, indent=2) + "\n")
|
|
1074
|
+
written.append("state.json")
|
|
1075
|
+
exp = run_dir / "experiments.jsonl"
|
|
1076
|
+
if not exp.exists():
|
|
1077
|
+
exp.touch()
|
|
1078
|
+
written.append("experiments.jsonl")
|
|
1079
|
+
qs = result["derived"]["questions"]
|
|
1080
|
+
qpath = run_dir / "QUESTIONS.md"
|
|
1081
|
+
if qs and not qpath.exists():
|
|
1082
|
+
lines = ["# Open questions (non_interactive run: the conservative choice was taken)", ""]
|
|
1083
|
+
for i, q in enumerate(qs, 1):
|
|
1084
|
+
lines += [f"{i}. **{q['question']}**", f" - Chosen: {q['choice']}", " - Stage: 1_intake", ""]
|
|
1085
|
+
qpath.write_text("\n".join(lines))
|
|
1086
|
+
written.append("QUESTIONS.md")
|
|
1087
|
+
return {"run_dir": str(run_dir), "resumed": resumed, "written": written, "intake_sha256": sha,
|
|
1088
|
+
"intake_source": source, "evidence_root": str(run_dir / "evidence")}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
# --------------------------------------------------------------------------- operations
|
|
1092
|
+
|
|
1093
|
+
TEMPLATE = """\
|
|
1094
|
+
# inferencekit intake v1. Validate with: inferencekit intake validate intake.yaml
|
|
1095
|
+
# Values in <angle brackets> (or __LIKE_THIS__) are placeholders; validate refuses them unless --allow-placeholders.
|
|
1096
|
+
intake_version: 1
|
|
1097
|
+
# run_id: my-model-5090-20261013 # default: <model>-<gpu>-<UTC date>; becomes runs/<run_id>/
|
|
1098
|
+
|
|
1099
|
+
model:
|
|
1100
|
+
id: <org/model-name> # Hugging Face id, or a local path
|
|
1101
|
+
revision: "<40-hex commit sha>" # pin it (quoted); a branch name is resolved to a sha at stage 1
|
|
1102
|
+
precision: <nvfp4|fp8|bf16|q8_0> # what the checkpoint is stored in
|
|
1103
|
+
# precision_allowed: [nvfp4] # default [precision]; engines may only run these (equal precision)
|
|
1104
|
+
kv_cache_dtype_allowed: [checkpoint_default] # adding fp8 (when the checkpoint does not ship it) is lossy
|
|
1105
|
+
# tokenizer: <org/model-name> # default: model.id
|
|
1106
|
+
|
|
1107
|
+
modality: llm # llm | tts | stt | video
|
|
1108
|
+
|
|
1109
|
+
hardware:
|
|
1110
|
+
gpu: RTX 5090
|
|
1111
|
+
count: 1
|
|
1112
|
+
price_usd_h: 0.60 # hourly price of the whole host (for $ caps and cost per 1M)
|
|
1113
|
+
access:
|
|
1114
|
+
kind: ssh # ssh: inferencekit and every engine run ON the GPU host
|
|
1115
|
+
host: <gpu-host>
|
|
1116
|
+
port: 22
|
|
1117
|
+
user: <user>
|
|
1118
|
+
key_path: ~/.ssh/<key>
|
|
1119
|
+
# access: {kind: local} # the GPU is on this machine
|
|
1120
|
+
|
|
1121
|
+
goals:
|
|
1122
|
+
lead: single_user # single_user | capacity (N* = streams per GPU inside the SLO)
|
|
1123
|
+
targets: # optional; omit the block for "as fast as possible"
|
|
1124
|
+
decode_tok_s: 150 # absolute, measured by bench serve (client-side timer)
|
|
1125
|
+
# ttft_ms: 120
|
|
1126
|
+
# decode_ceiling_fraction: 0.70 # relative to the measured-bandwidth roofline ceiling
|
|
1127
|
+
# n_star: 32 # capacity target; needs slo.ttft_p95_ms
|
|
1128
|
+
# usd_per_1m: 0.50
|
|
1129
|
+
# clip_ms: 60000 # modality video: client-timed ms per clip (or clips_per_hour)
|
|
1130
|
+
# buckets: [short, medium] # where the target must hold (default: every bucket)
|
|
1131
|
+
# after_target: stop # once every target is met: stop | keep_climbing (keep improving until the
|
|
1132
|
+
# # budget ends). Default keep_climbing when non_interactive or a cap is set
|
|
1133
|
+
slo: # required for capacity / n_star
|
|
1134
|
+
ttft_p95_ms: 500
|
|
1135
|
+
tpot_p95_ms: 50
|
|
1136
|
+
max_fail: 0
|
|
1137
|
+
# traffic: {arrival_rate_rps: 2.0, burstiness: poisson, session_len: "1 turn"}
|
|
1138
|
+
|
|
1139
|
+
io_modes:
|
|
1140
|
+
input: complete # complete | streaming (prompt arrives incrementally)
|
|
1141
|
+
output: streaming # complete | streaming
|
|
1142
|
+
# input_rate: "30 tok/s" # streaming input pace
|
|
1143
|
+
# latency_focus: "time to first token"
|
|
1144
|
+
|
|
1145
|
+
buckets: [very_short, short, medium, long] # 1-5 words / ~20-50 tok / ~300 tok / 2-4k tok
|
|
1146
|
+
|
|
1147
|
+
workload:
|
|
1148
|
+
max_tokens: 256 # generated per request, identical for every engine (ignore_eos)
|
|
1149
|
+
rounds: 5 # interleaved rounds across engines (>= 3)
|
|
1150
|
+
# concurrency: [1, 2, 4, 8, 16, 32, 64] # load-mode grid (capacity)
|
|
1151
|
+
# chat_template_kwargs: {enable_thinking: false} # binding pin: the manifest and every command must use it
|
|
1152
|
+
|
|
1153
|
+
budget:
|
|
1154
|
+
gpu_hours: 6 # GPU-hours = host hours x count
|
|
1155
|
+
usd: 10
|
|
1156
|
+
wall_clock_h: 6
|
|
1157
|
+
approved: true # required for non_interactive runs
|
|
1158
|
+
|
|
1159
|
+
fidelity: lossless # lossless: numerics as shipped | lossy: numeric changes inside quality.budget
|
|
1160
|
+
|
|
1161
|
+
quality:
|
|
1162
|
+
reference: none # none | {kind: endpoint, base_url: http://<host>:<port>/v1}
|
|
1163
|
+
noise_floor: measure # measure (A/A run) | a fixed agreement floor in [0, 1]
|
|
1164
|
+
# budget: # required when fidelity is lossy; every bound must hold vs the reference
|
|
1165
|
+
# gsm8k_drop_pts_max: 1.0 # GSM8K accuracy drop, percentage points
|
|
1166
|
+
# top1_agreement_min: 0.95 # next-token agreement, fraction
|
|
1167
|
+
# kl_max: 0.01 # mean KL per token, nats
|
|
1168
|
+
# nll_increase_max: 0.02 # reference NLL increase per token, nats
|
|
1169
|
+
# tv_max: 0.05 # mean total variation of the next-token distribution
|
|
1170
|
+
# (modality video: psnr_db_min, ssim_min, lpips_max instead, judged by quality clip)
|
|
1171
|
+
|
|
1172
|
+
repo: # the commit ledger; omit to skip it
|
|
1173
|
+
path: <path to a git repo on the machine running the agent>
|
|
1174
|
+
# branch: perf/<model>-<gpu> # default
|
|
1175
|
+
push: ask # ask | never (non_interactive never pushes)
|
|
1176
|
+
|
|
1177
|
+
constraints: # free text; honoured literally and quoted in the report
|
|
1178
|
+
- engines must be open source
|
|
1179
|
+
# - "speculation: off" # read by validate (quote it): speculation: off | auto (default auto),
|
|
1180
|
+
# - "route: build" # route: build | patch (default: build for single_user, patch for capacity)
|
|
1181
|
+
|
|
1182
|
+
non_interactive: false # true: never block; QUESTIONS.md + the conservative choice
|
|
1183
|
+
|
|
1184
|
+
# harness: # how to install inferencekit on an ssh GPU host
|
|
1185
|
+
# wheel: <path to inferencekit_cli-<version>-py3-none-any.whl>
|
|
1186
|
+
# wheel_sha256: <sha256>
|
|
1187
|
+
# candidates:
|
|
1188
|
+
# include: [] # engines to add (e.g. a custom engine with an OpenAI API)
|
|
1189
|
+
# exclude: []
|
|
1190
|
+
# max_engines: 5
|
|
1191
|
+
"""
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
@operation("intake_template", ("intake", "template"), "Print an annotated intake.yaml to start an "
|
|
1195
|
+
"optimizing-inference run from.",
|
|
1196
|
+
[Arg("out", "string", "Also write it to this file (refuses to overwrite)."),
|
|
1197
|
+
Arg("force", "boolean", "Overwrite --out if it exists.", default=False)])
|
|
1198
|
+
def intake_template(call, out, force):
|
|
1199
|
+
path = "stdout"
|
|
1200
|
+
if out:
|
|
1201
|
+
p = Path(out)
|
|
1202
|
+
if p.exists() and not force:
|
|
1203
|
+
raise OpError(f"{p} exists; pass --force to overwrite")
|
|
1204
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
1205
|
+
p.write_text(TEMPLATE)
|
|
1206
|
+
path = str(p)
|
|
1207
|
+
return {"format": "md", "markdown": TEMPLATE.rstrip("\n"), "path": path}
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
@operation("intake_validate", ("intake", "validate"), "Validate an intake.yaml: schema + cross-field rules, "
|
|
1211
|
+
"defaults filled, warnings, and the derived plan (stop rule, fidelity rule and quality budget, "
|
|
1212
|
+
"speculation, route, budget cap, stage time boxes). With "
|
|
1213
|
+
"--run-dir, also start or resume the run directory (state.json, QUESTIONS.md).",
|
|
1214
|
+
[Arg("file", "string", "Path to intake.yaml (or .json).", required=True, positional=True),
|
|
1215
|
+
Arg("run_dir", "string", "Start or resume this run directory (e.g. runs/<run-id>): writes a "
|
|
1216
|
+
"redacted copy of the intake (no host/user/port/key) and intake.normalized.json, and creates "
|
|
1217
|
+
"state.json / experiments.jsonl / QUESTIONS.md; never overwrites them. Refuses (writes nothing) "
|
|
1218
|
+
"if the run was started from a different intake: start a new run id."),
|
|
1219
|
+
Arg("allow_placeholders", "boolean", "Accept <placeholder> values (for checking templates).",
|
|
1220
|
+
default=False)])
|
|
1221
|
+
def intake_validate(call, file, run_dir, allow_placeholders):
|
|
1222
|
+
data, raw = load_file(file)
|
|
1223
|
+
access = (data.get("hardware") or {}).get("access") if isinstance(data.get("hardware"), dict) else None
|
|
1224
|
+
if isinstance(access, dict) and REDACTED in access.values():
|
|
1225
|
+
raise OpError(f"{file} is a redacted run-directory copy of an intake (access details removed); validate "
|
|
1226
|
+
"the original intake file instead (state.json: intake_source)")
|
|
1227
|
+
if run_dir and not data.get("run_id"):
|
|
1228
|
+
data["run_id"] = Path(run_dir).name # the run directory names the run
|
|
1229
|
+
result = check(data, allow_placeholders=allow_placeholders)
|
|
1230
|
+
result["intake_sha256"] = sha256_hex(raw)
|
|
1231
|
+
if run_dir and Path(run_dir).name != result["intake"]["run_id"]:
|
|
1232
|
+
result["warnings"].append(f"run_id {result['intake']['run_id']!r} differs from the run directory name "
|
|
1233
|
+
f"{Path(run_dir).name!r}")
|
|
1234
|
+
if run_dir:
|
|
1235
|
+
if result["placeholders"]:
|
|
1236
|
+
raise OpError("refusing to start a run with placeholder values: " + ", ".join(result["placeholders"]))
|
|
1237
|
+
result["run"] = start_run(Path(run_dir), Path(file), raw, result)
|
|
1238
|
+
return result
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def _parse_time(s: str) -> _dt.datetime:
|
|
1242
|
+
t = _dt.datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
1243
|
+
return t if t.tzinfo else t.replace(tzinfo=_dt.timezone.utc)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _hours(a: _dt.datetime, b: _dt.datetime) -> float:
|
|
1247
|
+
return max(0.0, (b - a).total_seconds() / 3600.0)
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def budget_status(state: dict, now: _dt.datetime) -> dict:
|
|
1251
|
+
b = state.get("budget") or {}
|
|
1252
|
+
price_default = b.get("price_usd_h")
|
|
1253
|
+
host_h, usd, open_segments = 0.0, 0.0, 0
|
|
1254
|
+
for seg in b.get("meter") or []:
|
|
1255
|
+
start = _parse_time(seg["start"])
|
|
1256
|
+
end = _parse_time(seg["end"]) if seg.get("end") else now
|
|
1257
|
+
open_segments += 0 if seg.get("end") else 1
|
|
1258
|
+
h = _hours(start, end)
|
|
1259
|
+
host_h += h
|
|
1260
|
+
price = seg.get("price_usd_h", price_default)
|
|
1261
|
+
usd += h * price if price else 0.0
|
|
1262
|
+
wall_h = _hours(_parse_time(b["run_started"]), now) if b.get("run_started") else None
|
|
1263
|
+
cap_h, cap_usd = b.get("host_hours_cap"), b.get("usd_cap")
|
|
1264
|
+
reserve_h = b.get("report_reserve_h", cap_h * REPORT_RESERVE if cap_h else None)
|
|
1265
|
+
left_h = cap_h - max(host_h, wall_h or 0.0) if cap_h else None
|
|
1266
|
+
left_usd = cap_usd - usd if cap_usd else None
|
|
1267
|
+
deadline = _parse_time(b["deadline"]) if b.get("deadline") else None
|
|
1268
|
+
deadline_left_h = (deadline - now).total_seconds() / 3600 if deadline else None
|
|
1269
|
+
if deadline is not None and reserve_h is None:
|
|
1270
|
+
duration = _hours(_parse_time(b["run_started"]), deadline) if b.get("run_started") else 0
|
|
1271
|
+
reserve_h = max(1 / 6, duration * REPORT_RESERVE)
|
|
1272
|
+
stage_key = state.get("current_stage")
|
|
1273
|
+
stage = (state.get("stages") or {}).get(stage_key) or {}
|
|
1274
|
+
stage_used = _hours(_parse_time(stage["started"]), now) if stage.get("started") else None
|
|
1275
|
+
box = stage.get("time_box_h") if b.get("stage_timeboxes", True) else None
|
|
1276
|
+
if ((left_h is not None and left_h <= 0) or (left_usd is not None and left_usd <= 0)
|
|
1277
|
+
or (deadline_left_h is not None and deadline_left_h <= 0)):
|
|
1278
|
+
verdict, action = "over_budget", "stop GPU work now; release the host; write the report (stage 8)"
|
|
1279
|
+
elif (stage_key != "8_report" and reserve_h is not None and
|
|
1280
|
+
((left_h is not None and left_h <= reserve_h) or
|
|
1281
|
+
(deadline_left_h is not None and deadline_left_h <= reserve_h))):
|
|
1282
|
+
verdict, action = "reserve_only", "only the report reserve is left: go to stage 8 now"
|
|
1283
|
+
elif box is not None and stage_used is not None and stage_used > box:
|
|
1284
|
+
verdict, action = "stage_over", (f"stage {stage_key} is past its {box} h time box: record what is "
|
|
1285
|
+
"unfinished as NOT RUN and move to the next stage")
|
|
1286
|
+
else:
|
|
1287
|
+
verdict, action = "ok", "continue"
|
|
1288
|
+
r3 = (lambda x: None if x is None else round(x, 3))
|
|
1289
|
+
return {"claim_class": "derived", "now": now.isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
1290
|
+
"verdict": verdict, "action": action,
|
|
1291
|
+
"spent": {"host_hours": r3(host_h), "usd": round(usd, 2), "wall_clock_h": r3(wall_h),
|
|
1292
|
+
"open_meter_segments": open_segments},
|
|
1293
|
+
"cap": {"host_hours": cap_h, "usd": cap_usd, "report_reserve_h": r3(reserve_h),
|
|
1294
|
+
"spend_cap": b.get("spend_cap", "bounded"), "deadline": b.get("deadline")},
|
|
1295
|
+
"left": {"host_hours": r3(left_h), "usd": None if left_usd is None else round(left_usd, 2),
|
|
1296
|
+
"deadline_h": r3(deadline_left_h)},
|
|
1297
|
+
"stage": {"key": stage_key, "used_h": r3(stage_used), "time_box_h": box},
|
|
1298
|
+
"formula": "spent host hours = sum of meter segments (open ones up to now); usd = hours x price; "
|
|
1299
|
+
"left = cap - max(host hours, wall clock)"}
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
@operation("intake_budget", ("intake", "budget"), "Budget and time-box check for a run directory: spent and left "
|
|
1303
|
+
"(host hours, USD, wall clock) and the current stage's time box, with a verdict "
|
|
1304
|
+
"(ok | stage_over | reserve_only | over_budget). Read-only.",
|
|
1305
|
+
[Arg("run_dir", "string", "The run directory (runs/<run-id>) holding state.json.", required=True,
|
|
1306
|
+
positional=True),
|
|
1307
|
+
Arg("now", "string", "Evaluate at this ISO-8601 UTC time instead of the current time.")])
|
|
1308
|
+
def intake_budget(call, run_dir, now):
|
|
1309
|
+
path = Path(run_dir) / "state.json"
|
|
1310
|
+
if not path.is_file():
|
|
1311
|
+
raise OpError(f"{path} not found: start the run with 'inferencekit intake validate <file> --run-dir "
|
|
1312
|
+
f"{run_dir}'")
|
|
1313
|
+
try:
|
|
1314
|
+
state = json.loads(path.read_text())
|
|
1315
|
+
except ValueError:
|
|
1316
|
+
raise OpError(f"{path} is not valid JSON") from None
|
|
1317
|
+
try:
|
|
1318
|
+
t = _parse_time(now) if now else _dt.datetime.now(_dt.timezone.utc)
|
|
1319
|
+
return {"run_dir": str(run_dir), **budget_status(state, t)}
|
|
1320
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
1321
|
+
raise OpError(f"{path}: malformed budget/stage fields ({e})") from None
|