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,518 @@
|
|
|
1
|
+
"""Build the prompt sets from openly licensed sources, deterministically.
|
|
2
|
+
|
|
3
|
+
Sources (downloaded once into a cache directory; their sha256 is recorded in every
|
|
4
|
+
manifest, so a rebuild from different bytes is visible):
|
|
5
|
+
|
|
6
|
+
* databricks-dolly-15k (CC BY-SA 3.0): instructions without context feed very_short
|
|
7
|
+
and short; context + instruction pairs feed medium.
|
|
8
|
+
* GSM8K train split (MIT): grade-school maths questions feed short.
|
|
9
|
+
* Public-domain novels (Project Gutenberg plain-text editions, licence boilerplate
|
|
10
|
+
stripped): passages of ~1,700-2,600 words plus a question feed long.
|
|
11
|
+
|
|
12
|
+
Groups and the dev / sealed split: every candidate belongs to a *group*, the unit that is
|
|
13
|
+
split and deduplicated. A dolly row with a context is grouped by the sha256 of its
|
|
14
|
+
normalised context paragraph (many rows ask different questions about one paragraph), a
|
|
15
|
+
row without context by its normalised instruction, a GSM8K question by its normalised
|
|
16
|
+
text, and a novel passage by its book. Each group yields at most one candidate (novels:
|
|
17
|
+
one per passage), and a group is assigned to ``dev`` or ``sealed`` by one bit of
|
|
18
|
+
``sha256(seed | split | group)``, so rows that share a context paragraph always land on
|
|
19
|
+
the same side, and no two prompts anywhere share a context (no shared prefix for a
|
|
20
|
+
prefix cache to reuse). As a second guard, a sealed candidate is dropped when more than
|
|
21
|
+
half of its 12-word shingles appear in the dev sets, and any candidate is dropped when
|
|
22
|
+
more than half of its shingles appear in an item already chosen for the same split.
|
|
23
|
+
Inside a split, groups are ranked by another seeded hash; the single-user set takes the
|
|
24
|
+
first 16 per bucket and the load set the next ones, so the four sets are disjoint.
|
|
25
|
+
|
|
26
|
+
The code and the seed are public, so a sealed set built from the public seed alone could
|
|
27
|
+
be rebuilt by anyone, including an agent being evaluated. ``prompts seal`` therefore
|
|
28
|
+
takes a secret: the sealed sets are drawn from every group the dev sets do not use and
|
|
29
|
+
ranked by ``sha256(secret | ...)``. The manifests commit to the secret by
|
|
30
|
+
``secret_sha256`` = the sha256 of the secret file's bytes as ``prompts seal`` writes them
|
|
31
|
+
(64 hex characters and a newline), i.e. ``sha256sum SEED.secret``. Revealing the file
|
|
32
|
+
after the evaluation lets anyone check that hash, rebuild the sealed set
|
|
33
|
+
(``prompts verify --secret-file``) and compare it with the pre-registered sha256.
|
|
34
|
+
|
|
35
|
+
Exclusions: text the release leak scanner would flag (names that must never appear in a
|
|
36
|
+
tracked file) is excluded. The scanner's rules are private and never in the repository;
|
|
37
|
+
they are read at build time only from a file given explicitly, with ``--blocklist FILE`` or
|
|
38
|
+
``$INFERENCEKIT_PROMPT_BLOCKLIST`` (scanner syntax). The source rows they
|
|
39
|
+
exclude are recorded, by position only, in ``exclusions.json`` next to the shipped sets
|
|
40
|
+
(keyed by the source file's sha256), so a public rebuild without the private rules
|
|
41
|
+
excludes the same rows and gives byte-identical sets.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import hashlib
|
|
47
|
+
import json
|
|
48
|
+
import os
|
|
49
|
+
import re
|
|
50
|
+
import urllib.request
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
|
|
53
|
+
from inferencekit.prompts import BUCKET_RULES, BUCKETS, PROXY_FACTOR, gzip_bytes, in_bucket, proxy_tokens, words
|
|
54
|
+
|
|
55
|
+
SEED = "inferencekit-prompts-v1"
|
|
56
|
+
VERSION = "1.0"
|
|
57
|
+
MAX_TOKENS = 256
|
|
58
|
+
PER_BUCKET = 16
|
|
59
|
+
LOAD_TARGET = 1280 # >= 1,200 unique short/medium prompts per load set
|
|
60
|
+
LOAD_MEDIUM_MAX = 280
|
|
61
|
+
|
|
62
|
+
DOLLY = {"name": "databricks-dolly-15k", "license": "CC-BY-SA-3.0", "file": "dolly.jsonl",
|
|
63
|
+
"url": "https://huggingface.co/datasets/databricks/databricks-dolly-15k/resolve/"
|
|
64
|
+
"bdd27f4d94b9c1f951818a7da7fd7aeea5dbff1a/databricks-dolly-15k.jsonl"}
|
|
65
|
+
GSM8K = {"name": "gsm8k-train", "license": "MIT", "file": "gsm8k_train.jsonl",
|
|
66
|
+
"url": "https://raw.githubusercontent.com/openai/grade-school-math/"
|
|
67
|
+
"3101c7d5072418e28b9008a6636bde82a006892c/grade_school_math/data/train.jsonl"}
|
|
68
|
+
# Public-domain novels (US). Gutenberg ebook numbers -> title, for attribution.
|
|
69
|
+
BOOKS = {
|
|
70
|
+
11: "Alice's Adventures in Wonderland (Lewis Carroll)", 35: "The Time Machine (H. G. Wells)",
|
|
71
|
+
36: "The War of the Worlds (H. G. Wells)", 43: "The Strange Case of Dr Jekyll and Mr Hyde (R. L. Stevenson)",
|
|
72
|
+
76: "Adventures of Huckleberry Finn (Mark Twain)", 84: "Frankenstein (Mary Shelley)",
|
|
73
|
+
98: "A Tale of Two Cities (Charles Dickens)", 120: "Treasure Island (R. L. Stevenson)",
|
|
74
|
+
158: "Emma (Jane Austen)", 161: "Sense and Sensibility (Jane Austen)",
|
|
75
|
+
174: "The Picture of Dorian Gray (Oscar Wilde)", 215: "The Call of the Wild (Jack London)",
|
|
76
|
+
219: "Heart of Darkness (Joseph Conrad)", 345: "Dracula (Bram Stoker)",
|
|
77
|
+
768: "Wuthering Heights (Emily Bronte)", 1260: "Jane Eyre (Charlotte Bronte)",
|
|
78
|
+
1342: "Pride and Prejudice (Jane Austen)", 1400: "Great Expectations (Charles Dickens)",
|
|
79
|
+
1661: "The Adventures of Sherlock Holmes (Arthur Conan Doyle)",
|
|
80
|
+
2554: "Crime and Punishment (Fyodor Dostoevsky, tr. Constance Garnett)",
|
|
81
|
+
2701: "Moby Dick (Herman Melville)",
|
|
82
|
+
}
|
|
83
|
+
GUTENBERG_URL = "https://www.gutenberg.org/cache/epub/{n}/pg{n}.txt"
|
|
84
|
+
GUTENBERG_LICENSE = "public-domain"
|
|
85
|
+
LONG_QUESTIONS = (
|
|
86
|
+
"Summarize the passage above in about 150 words.",
|
|
87
|
+
"List the characters who appear in the passage above and describe what each of them does.",
|
|
88
|
+
"What is the central conflict or tension in the passage above? Support your answer with two details from the text.",
|
|
89
|
+
"Describe the setting of the passage above and how it shapes the mood.",
|
|
90
|
+
"Write five questions a teacher could ask about the passage above, each with a one-sentence answer.",
|
|
91
|
+
"Explain what happens in the passage above, step by step, in the order the events occur.",
|
|
92
|
+
)
|
|
93
|
+
PASSAGE_FRACTIONS = (0.1, 0.25, 0.4, 0.55, 0.7, 0.85) # >= 3,000 words apart in every book used
|
|
94
|
+
EXCLUSIONS_FILE = "exclusions.json"
|
|
95
|
+
SHINGLE = 12 # words per shingle for the near-duplicate guard
|
|
96
|
+
MAX_SHARED = 0.5 # largest share of a candidate's shingles allowed to appear in the other split / same set
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --------------------------------------------------------------------------- sources
|
|
100
|
+
|
|
101
|
+
def source_files(cache: Path) -> dict[str, Path]:
|
|
102
|
+
files = {"dolly": cache / DOLLY["file"], "gsm8k": cache / GSM8K["file"]}
|
|
103
|
+
for n in BOOKS:
|
|
104
|
+
files[f"gutenberg:{n}"] = cache / "gutenberg" / f"pg{n}.txt"
|
|
105
|
+
return files
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def fetch(cache: Path, log=print) -> dict[str, Path]:
|
|
109
|
+
"""Download any missing source file into ``cache``. Returns the source map."""
|
|
110
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
(cache / "gutenberg").mkdir(exist_ok=True)
|
|
112
|
+
urls = {"dolly": DOLLY["url"], "gsm8k": GSM8K["url"]}
|
|
113
|
+
urls.update({f"gutenberg:{n}": GUTENBERG_URL.format(n=n) for n in BOOKS})
|
|
114
|
+
files = source_files(cache)
|
|
115
|
+
for key, path in files.items():
|
|
116
|
+
if path.is_file() and path.stat().st_size > 0:
|
|
117
|
+
continue
|
|
118
|
+
log(f"downloading {urls[key]}")
|
|
119
|
+
req = urllib.request.Request(urls[key], headers={"User-Agent": "inferencekit-prompt-builder"})
|
|
120
|
+
with urllib.request.urlopen(req, timeout=120) as r: # noqa: S310 - fixed https URLs above
|
|
121
|
+
data = r.read()
|
|
122
|
+
tmp = path.with_suffix(path.suffix + ".part")
|
|
123
|
+
tmp.write_bytes(data)
|
|
124
|
+
tmp.replace(path)
|
|
125
|
+
return files
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def source_table(cache: Path) -> list[dict]:
|
|
129
|
+
files = source_files(cache)
|
|
130
|
+
rows = [{"name": DOLLY["name"], "license": DOLLY["license"], "url": DOLLY["url"],
|
|
131
|
+
"sha256": sha256_file(files["dolly"])},
|
|
132
|
+
{"name": GSM8K["name"], "license": GSM8K["license"], "url": GSM8K["url"],
|
|
133
|
+
"sha256": sha256_file(files["gsm8k"])}]
|
|
134
|
+
for n, title in BOOKS.items():
|
|
135
|
+
rows.append({"name": f"gutenberg-{n}", "title": title, "license": GUTENBERG_LICENSE,
|
|
136
|
+
"url": GUTENBERG_URL.format(n=n), "sha256": sha256_file(files[f"gutenberg:{n}"])})
|
|
137
|
+
return rows
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --------------------------------------------------------------------------- hashing
|
|
141
|
+
|
|
142
|
+
def _h(*parts) -> str:
|
|
143
|
+
return hashlib.sha256("|".join(str(p) for p in parts).encode("utf-8")).hexdigest()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def split_of(key: str, seed: str = SEED) -> str:
|
|
147
|
+
"""'dev' or 'sealed' from one bit of a seeded hash of the source item's key."""
|
|
148
|
+
return "dev" if int(_h(seed, "split", key)[:2], 16) % 2 == 0 else "sealed"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _norm(text: str) -> str:
|
|
152
|
+
return " ".join(text.lower().split())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def blocklist_path(explicit: str | None = None) -> Path | None:
|
|
156
|
+
"""The private scrub-patterns file, only when one is named: ``explicit`` (the ``--blocklist``
|
|
157
|
+
flag), else ``$INFERENCEKIT_PROMPT_BLOCKLIST``. Nothing is read by default; ``none`` or an
|
|
158
|
+
empty value means no file. A named file that does not exist is an error."""
|
|
159
|
+
value = explicit if explicit is not None else os.environ.get("INFERENCEKIT_PROMPT_BLOCKLIST")
|
|
160
|
+
if value is None or not value.strip() or value.strip().lower() == "none":
|
|
161
|
+
return None
|
|
162
|
+
p = Path(value.strip()).expanduser()
|
|
163
|
+
if not p.is_file():
|
|
164
|
+
raise FileNotFoundError(f"blocklist file not found: {p}")
|
|
165
|
+
return p
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_rules(path: Path | None) -> list[re.Pattern]:
|
|
169
|
+
"""Byte regexes from a scrub-patterns file (the release scanner's syntax: ``literal:TEXT``,
|
|
170
|
+
``regex:PATTERN``, ``TEXT``, each optionally ``==>REPLACEMENT``; ``#scan RULE`` is a rule too;
|
|
171
|
+
other ``#`` lines are comments or directives)."""
|
|
172
|
+
if path is None:
|
|
173
|
+
return []
|
|
174
|
+
rules = []
|
|
175
|
+
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
|
176
|
+
raw = line.rstrip("\r\n")
|
|
177
|
+
if not raw.strip() or raw.startswith("#@"):
|
|
178
|
+
continue
|
|
179
|
+
if raw.startswith("#scan "):
|
|
180
|
+
raw = raw[len("#scan "):]
|
|
181
|
+
elif raw.startswith("#"):
|
|
182
|
+
continue
|
|
183
|
+
match = raw.rsplit("==>", 1)[0] if "==>" in raw else raw
|
|
184
|
+
if match.startswith("regex:"):
|
|
185
|
+
rules.append(re.compile(match[len("regex:"):].encode("utf-8")))
|
|
186
|
+
else:
|
|
187
|
+
text = match[len("literal:"):] if match.startswith("literal:") else match
|
|
188
|
+
if text:
|
|
189
|
+
rules.append(re.compile(re.escape(text.encode("utf-8"))))
|
|
190
|
+
return rules
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def blocked(text: str, rules: list[re.Pattern]) -> bool:
|
|
194
|
+
"""True if the scanner would flag the text (as written, or as a JSONL string)."""
|
|
195
|
+
if not rules:
|
|
196
|
+
return False
|
|
197
|
+
forms = (text.encode("utf-8"), json.dumps(text, ensure_ascii=False).encode("utf-8"))
|
|
198
|
+
return any(r.search(f) for r in rules for f in forms)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def read_exclusions(path: Path) -> dict[str, set[str]]:
|
|
202
|
+
"""{source sha256: {row refs}} from an exclusions file (empty if there is none)."""
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
205
|
+
except (OSError, ValueError):
|
|
206
|
+
return {}
|
|
207
|
+
return {sha: set(refs) for sha, refs in (data.get("excluded") or {}).items()}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def write_exclusions(path: Path, excluded: dict[str, set[str]], names: dict[str, str]) -> None:
|
|
211
|
+
data = {"format": 1,
|
|
212
|
+
"rule": "source rows the release leak scanner's private rules would flag; listed by position in the "
|
|
213
|
+
"source file (keyed by that file's sha256) so a rebuild without the private rules matches",
|
|
214
|
+
"sources": {sha: names.get(sha, "?") for sha in sorted(excluded)},
|
|
215
|
+
"excluded": {sha: sorted(refs, key=_ref_order) for sha, refs in sorted(excluded.items()) if refs}}
|
|
216
|
+
Path(path).write_text(json.dumps(data, indent=1) + "\n", encoding="utf-8")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _ref_order(ref: str):
|
|
220
|
+
return tuple(int(x) if x.isdigit() else x for x in re.split(r"[:]", ref.replace("row", "")))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def exclusions_digest(excluded: dict[str, set[str]]) -> str:
|
|
224
|
+
"""sha256 over the sorted (source sha256, ref) pairs actually applied: recorded in each manifest."""
|
|
225
|
+
lines = sorted(f"{sha} {ref}" for sha, refs in excluded.items() for ref in refs)
|
|
226
|
+
return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def shingles(text: str, n: int = SHINGLE) -> set[str]:
|
|
230
|
+
w = text.lower().split()
|
|
231
|
+
return {" ".join(w[i:i + n]) for i in range(len(w) - n + 1)}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def shared_fraction(sh: set[str], seen: set[str]) -> float:
|
|
235
|
+
return len(sh & seen) / len(sh) if sh else 0.0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# --------------------------------------------------------------------------- candidates
|
|
239
|
+
|
|
240
|
+
def _dolly_candidates(path: Path) -> list[dict]:
|
|
241
|
+
"""One candidate per group: a context paragraph (rows asking about the same paragraph are one
|
|
242
|
+
group) or, without context, a normalised instruction. The first row in file order that fits a
|
|
243
|
+
bucket represents its group."""
|
|
244
|
+
out, seen = [], set()
|
|
245
|
+
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines()):
|
|
246
|
+
if not line.strip():
|
|
247
|
+
continue
|
|
248
|
+
o = json.loads(line)
|
|
249
|
+
ins, ctx = " ".join(o["instruction"].split()), o.get("context", "").strip()
|
|
250
|
+
if not ins:
|
|
251
|
+
continue
|
|
252
|
+
if ctx:
|
|
253
|
+
ctx = "\n\n".join(" ".join(p.split()) for p in re.split(r"\n\s*\n", ctx) if p.strip())
|
|
254
|
+
text, bucket = f"{ctx}\n\n{ins}", "medium"
|
|
255
|
+
key = "dolly-context:" + hashlib.sha256(_norm(ctx).encode("utf-8")).hexdigest()
|
|
256
|
+
if not in_bucket(text, "medium"):
|
|
257
|
+
continue
|
|
258
|
+
else:
|
|
259
|
+
text = ins
|
|
260
|
+
key = "dolly:" + _norm(text)
|
|
261
|
+
if in_bucket(text, "very_short") and text.endswith("?"):
|
|
262
|
+
bucket = "very_short"
|
|
263
|
+
elif in_bucket(text, "short"):
|
|
264
|
+
bucket = "short"
|
|
265
|
+
else:
|
|
266
|
+
continue
|
|
267
|
+
if key in seen:
|
|
268
|
+
continue
|
|
269
|
+
seen.add(key)
|
|
270
|
+
out.append({"bucket": bucket, "text": text, "key": key, "ref": f"dolly:row{n + 1}", "src": "dolly",
|
|
271
|
+
"source": f"{DOLLY['name']}/{DOLLY['license']}", "source_ref": f"{DOLLY['name']}:line{n + 1}"})
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _gsm8k_candidates(path: Path) -> list[dict]:
|
|
276
|
+
out, seen = [], set()
|
|
277
|
+
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines()):
|
|
278
|
+
if not line.strip():
|
|
279
|
+
continue
|
|
280
|
+
q = " ".join(json.loads(line)["question"].split())
|
|
281
|
+
key = "gsm8k:" + _norm(q)
|
|
282
|
+
if key in seen or not in_bucket(q, "short"):
|
|
283
|
+
continue
|
|
284
|
+
seen.add(key)
|
|
285
|
+
out.append({"bucket": "short", "text": q, "key": key, "ref": f"gsm8k:row{n + 1}", "src": "gsm8k",
|
|
286
|
+
"source": f"{GSM8K['name']}/{GSM8K['license']}", "source_ref": f"{GSM8K['name']}:line{n + 1}"})
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def gutenberg_paragraphs(raw: str) -> list[str]:
|
|
291
|
+
"""Body paragraphs with the distributor's licence header and footer removed."""
|
|
292
|
+
text = raw.replace("\r\n", "\n").lstrip("")
|
|
293
|
+
start = re.search(r"^\*\*\* ?START OF.*$", text, re.M)
|
|
294
|
+
end = re.search(r"^\*\*\* ?END OF.*$", text, re.M)
|
|
295
|
+
body = text[start.end() if start else 0: end.start() if end else len(text)]
|
|
296
|
+
paras = [" ".join(p.split()) for p in re.split(r"\n\s*\n", body) if p.strip()]
|
|
297
|
+
return [p for p in paras if "gutenberg" not in p.lower()]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _long_candidates(files: dict[str, Path], seed: str) -> list[dict]:
|
|
301
|
+
out = []
|
|
302
|
+
for n in BOOKS:
|
|
303
|
+
paras = gutenberg_paragraphs(files[f"gutenberg:{n}"].read_text(encoding="utf-8", errors="replace"))
|
|
304
|
+
for k, frac in enumerate(PASSAGE_FRACTIONS):
|
|
305
|
+
target = 1700 + int(_h(seed, "len", n, k)[:4], 16) % 900 # 1,700-2,599 words
|
|
306
|
+
i = int(len(paras) * frac)
|
|
307
|
+
chunk, count = [], 0
|
|
308
|
+
while i < len(paras) and count < target:
|
|
309
|
+
chunk.append(paras[i])
|
|
310
|
+
count += words(paras[i])
|
|
311
|
+
i += 1
|
|
312
|
+
question = LONG_QUESTIONS[int(_h(seed, "q", n, k)[:4], 16) % len(LONG_QUESTIONS)]
|
|
313
|
+
passage = "\n\n".join(chunk)
|
|
314
|
+
text = ("Read the following passage from a public-domain novel and answer the question after it.\n\n"
|
|
315
|
+
f"{passage}\n\nQuestion: {question}")
|
|
316
|
+
if not in_bucket(text, "long"):
|
|
317
|
+
continue
|
|
318
|
+
out.append({"bucket": "long", "text": text, "key": f"gutenberg:{n}", "rank_key": f"gutenberg:{n}:{k}",
|
|
319
|
+
"ref": f"gutenberg:{n}:passage{k}", "src": f"gutenberg:{n}",
|
|
320
|
+
"order": k, "source": f"gutenberg-{n}/{GUTENBERG_LICENSE}",
|
|
321
|
+
"source_ref": f"{BOOKS[n]}, passage at {int(frac * 100)}% of the book"})
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def candidates(cache: Path, seed: str = SEED, exclusions: dict[str, set[str]] | None = None,
|
|
326
|
+
rules: list[re.Pattern] | None = None) -> tuple[list[dict], dict[str, set[str]]]:
|
|
327
|
+
"""(candidates, exclusions applied). A row is excluded when an exclusions file lists it for this
|
|
328
|
+
exact source file (by sha256) or a private scanner rule matches its text."""
|
|
329
|
+
files = source_files(cache)
|
|
330
|
+
missing = [str(p) for p in files.values() if not p.is_file()]
|
|
331
|
+
if missing:
|
|
332
|
+
raise FileNotFoundError(f"missing source files (run with fetch): {missing[:3]}")
|
|
333
|
+
shas = {k: sha256_file(p) for k, p in files.items()}
|
|
334
|
+
cands = _dolly_candidates(files["dolly"]) + _gsm8k_candidates(files["gsm8k"]) + _long_candidates(files, seed)
|
|
335
|
+
listed = exclusions or {}
|
|
336
|
+
applied: dict[str, set[str]] = {}
|
|
337
|
+
kept = []
|
|
338
|
+
for c in cands:
|
|
339
|
+
sha = shas[c["src"]]
|
|
340
|
+
if c["ref"] in listed.get(sha, ()) or blocked(c["text"], rules or []):
|
|
341
|
+
applied.setdefault(sha, set()).add(c["ref"])
|
|
342
|
+
continue
|
|
343
|
+
c["split"] = split_of(c["key"], seed)
|
|
344
|
+
kept.append(c)
|
|
345
|
+
return kept, applied
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def source_names(cache: Path) -> dict[str, str]:
|
|
349
|
+
"""{sha256: source name} for the exclusions file."""
|
|
350
|
+
files = source_files(cache)
|
|
351
|
+
names = {"dolly": DOLLY["name"], "gsm8k": GSM8K["name"]}
|
|
352
|
+
return {sha256_file(p): names.get(k, k.replace(":", "-")) for k, p in files.items()}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# --------------------------------------------------------------------------- selection
|
|
356
|
+
|
|
357
|
+
def _item(c: dict, set_tag: str, i: int) -> dict:
|
|
358
|
+
return {"id": f"{c['bucket']}-{set_tag}{i:04d}", "bucket": c["bucket"],
|
|
359
|
+
"messages": [{"role": "user", "content": c["text"]}], "max_tokens": MAX_TOKENS,
|
|
360
|
+
"source": c["source"], "kind": "chat", "source_ref": c["source_ref"],
|
|
361
|
+
"proxy_tokens": proxy_tokens(c["text"])}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _pick(cands: list[dict], split: str, seed: str, secret: str | None) -> tuple[dict, dict]:
|
|
365
|
+
"""({bucket: single candidates}, {bucket: load candidates}) for one split."""
|
|
366
|
+
pools: dict[str, list[dict]] = {b: [] for b in BUCKETS}
|
|
367
|
+
avoid: set[str] = set() # shingles of every dev prompt (sealed only)
|
|
368
|
+
if split == "sealed":
|
|
369
|
+
# Never a group the dev sets use, and never text that mostly repeats a dev prompt.
|
|
370
|
+
dev_single, dev_load = _pick(cands, "dev", seed, None)
|
|
371
|
+
dev = [c for group in (dev_single, dev_load) for cs in group.values() for c in cs]
|
|
372
|
+
used = {c["key"] for c in dev}
|
|
373
|
+
for c in dev:
|
|
374
|
+
avoid |= shingles(c["text"])
|
|
375
|
+
for c in cands:
|
|
376
|
+
# With a secret: every group the dev sets do not use (long passages: every book they do
|
|
377
|
+
# not use), ranked by the secret, so nobody can predict the sealed set without it and
|
|
378
|
+
# anyone can rebuild it once it is revealed. Without one: the public sealed half.
|
|
379
|
+
if c["key"] not in used and (secret or c["split"] == "sealed"):
|
|
380
|
+
pools[c["bucket"]].append(c)
|
|
381
|
+
salt = secret or seed
|
|
382
|
+
else:
|
|
383
|
+
for c in cands:
|
|
384
|
+
if c["split"] == split:
|
|
385
|
+
pools[c["bucket"]].append(c)
|
|
386
|
+
salt = seed
|
|
387
|
+
seen: set[str] = set()
|
|
388
|
+
for b in BUCKETS:
|
|
389
|
+
# Long passages: one per book first (order k), then the next passage of each book.
|
|
390
|
+
pool = sorted(pools[b], key=lambda c: (c.get("order", 0), _h(salt, "rank", split, b,
|
|
391
|
+
c.get("rank_key", c["key"]))))
|
|
392
|
+
kept = []
|
|
393
|
+
for c in pool: # near-duplicate guard, in rank order
|
|
394
|
+
sh = shingles(c["text"])
|
|
395
|
+
if shared_fraction(sh, avoid) > MAX_SHARED or shared_fraction(sh, seen) > MAX_SHARED:
|
|
396
|
+
continue
|
|
397
|
+
seen |= sh
|
|
398
|
+
kept.append(c)
|
|
399
|
+
pools[b] = kept
|
|
400
|
+
single, rest = {}, {}
|
|
401
|
+
for b in BUCKETS:
|
|
402
|
+
if len(pools[b]) < PER_BUCKET:
|
|
403
|
+
raise ValueError(f"{split}: only {len(pools[b])} {b} candidates (need {PER_BUCKET})")
|
|
404
|
+
single[b] = pools[b][:PER_BUCKET]
|
|
405
|
+
rest[b] = pools[b][PER_BUCKET:]
|
|
406
|
+
n_med = min(LOAD_MEDIUM_MAX, len(rest["medium"]))
|
|
407
|
+
n_short = LOAD_TARGET - n_med
|
|
408
|
+
if len(rest["short"]) < n_short:
|
|
409
|
+
raise ValueError(f"{split}: only {len(rest['short'])} short load candidates (need {n_short})")
|
|
410
|
+
return single, {"short": rest["short"][:n_short], "medium": rest["medium"][:n_med]}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def select(cands: list[dict], split: str, seed: str = SEED, secret: str | None = None) -> dict[str, list[dict]]:
|
|
414
|
+
"""{'single': [...], 'load': [...]} for one split. Disjoint by construction.
|
|
415
|
+
|
|
416
|
+
``dev``: the dev half of the public hash split, ranked by the public seed.
|
|
417
|
+
``sealed`` without a secret: the other half, ranked by the public seed (reproducible by
|
|
418
|
+
anyone, so only "sealed" against accidents). ``sealed`` with a secret: see ``_pick``.
|
|
419
|
+
"""
|
|
420
|
+
tags = {"dev": ("", "load-"), "sealed": ("s", "sload-")}[split]
|
|
421
|
+
single, load = _pick(cands, split, seed, secret)
|
|
422
|
+
out_single = [_item(c, tags[0], i + 1) for b in BUCKETS for i, c in enumerate(single[b])]
|
|
423
|
+
out_load = [_item(c, tags[1], i + 1) for b in ("short", "medium") for i, c in enumerate(load[b])]
|
|
424
|
+
return {"single": out_single, "load": out_load}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def sha256_file(path: Path) -> str:
|
|
429
|
+
"""sha256 of a file's raw bytes (source files, manifests)."""
|
|
430
|
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# --------------------------------------------------------------------------- writing
|
|
434
|
+
|
|
435
|
+
def jsonl_bytes(items: list[dict]) -> bytes:
|
|
436
|
+
return "".join(json.dumps(it, ensure_ascii=False) + "\n" for it in items).encode("utf-8")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
SECRET_RULE = ("secret_sha256 = sha256 of the secret file's bytes as prompts seal writes them (64 hex characters and "
|
|
440
|
+
"a newline): `sha256sum SEED.secret`, or `echo <secret> | sha256sum`")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def manifest(name: str, items: list[dict], data: bytes, sealed: bool, role: str, sources: list[dict],
|
|
444
|
+
seed: str = SEED, secret: str | None = None, secret_sha256: str | None = None,
|
|
445
|
+
exclusions: dict | None = None) -> dict:
|
|
446
|
+
counts = {b: sum(1 for it in items if it["bucket"] == b) for b in BUCKETS}
|
|
447
|
+
return {"name": name, "version": VERSION, "n": len(items), "buckets": {b: n for b, n in counts.items() if n},
|
|
448
|
+
"sha256": hashlib.sha256(data).hexdigest(), "sealed": sealed, "role": role, "seed": seed,
|
|
449
|
+
"max_tokens": MAX_TOKENS,
|
|
450
|
+
"length_proxy": f"proxy_tokens = whitespace words * {PROXY_FACTOR}", "bucket_rules": BUCKET_RULES,
|
|
451
|
+
"split_rule": "sha256(seed|split|group): even first byte -> dev, odd -> sealed; group = sha256 of the "
|
|
452
|
+
"normalised dolly context (else the normalised instruction / question; long: the book); "
|
|
453
|
+
"one candidate per group; near-duplicates (>50% shared 12-word shingles with the dev sets "
|
|
454
|
+
"or the same split) dropped",
|
|
455
|
+
"selection": ("secret-seeded: groups not used by dev/dev-load (long: books not used), ranked by "
|
|
456
|
+
"sha256(secret|rank|...); the secret is committed by secret_sha256 and revealed after use"
|
|
457
|
+
if secret else "ranked by sha256(seed|rank|split|bucket|key) within the split"),
|
|
458
|
+
"secret_sha256": secret_sha256 if secret else None,
|
|
459
|
+
"secret_rule": SECRET_RULE if secret else None,
|
|
460
|
+
"exclusions": {"rows": sum(len(v) for v in (exclusions or {}).values()),
|
|
461
|
+
"sha256": exclusions_digest(exclusions or {}),
|
|
462
|
+
"rule": "rows listed in exclusions.json or flagged by the private leak-scanner rules"},
|
|
463
|
+
"sources": sources, "builder": "scripts/build_prompts.py (inferencekit prompts build / seal)"}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def write_set(out_dir: Path, name: str, items: list[dict], sealed: bool, role: str, sources: list[dict],
|
|
467
|
+
seed: str = SEED, secret: str | None = None, secret_sha256: str | None = None,
|
|
468
|
+
exclusions: dict | None = None, gz: bool = False) -> dict:
|
|
469
|
+
"""Write ``<name>.jsonl`` (``gz``: ``<name>.jsonl.gz``, removing a stale twin) and its manifest.
|
|
470
|
+
The manifest's sha256 is of the uncompressed JSONL bytes either way."""
|
|
471
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
472
|
+
data = jsonl_bytes(items)
|
|
473
|
+
plain, packed = out_dir / f"{name}.jsonl", out_dir / f"{name}.jsonl.gz"
|
|
474
|
+
if gz:
|
|
475
|
+
packed.write_bytes(gzip_bytes(data))
|
|
476
|
+
plain.unlink(missing_ok=True)
|
|
477
|
+
else:
|
|
478
|
+
plain.write_bytes(data)
|
|
479
|
+
packed.unlink(missing_ok=True)
|
|
480
|
+
man = manifest(name, items, data, sealed, role, sources, seed, secret, secret_sha256, exclusions)
|
|
481
|
+
(out_dir / f"{name}.manifest.json").write_text(json.dumps(man, indent=2, ensure_ascii=False) + "\n",
|
|
482
|
+
encoding="utf-8")
|
|
483
|
+
return man
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def build_split(cache: Path, out_dir: Path, split: str, seed: str = SEED, secret: str | None = None,
|
|
487
|
+
secret_sha256: str | None = None, exclusions: dict[str, set[str]] | None = None,
|
|
488
|
+
rules: list[re.Pattern] | None = None, gz: bool = False) -> tuple[list[dict], dict[str, set[str]]]:
|
|
489
|
+
"""Write ``<split>.jsonl`` and ``<split>-load.jsonl`` (``gz``: ``.jsonl.gz``) (+ manifests) into ``out_dir``.
|
|
490
|
+
|
|
491
|
+
Returns (manifests, exclusions applied)."""
|
|
492
|
+
cands, applied = candidates(cache, seed, exclusions, rules)
|
|
493
|
+
sets = select(cands, split, seed, secret)
|
|
494
|
+
sources = source_table(cache)
|
|
495
|
+
sealed = split == "sealed"
|
|
496
|
+
mans = [write_set(out_dir, split, sets["single"], sealed, "single", sources, seed, secret, secret_sha256, applied,
|
|
497
|
+
gz=gz),
|
|
498
|
+
write_set(out_dir, f"{split}-load", sets["load"], sealed, "load", sources, seed, secret, secret_sha256,
|
|
499
|
+
applied, gz=gz)]
|
|
500
|
+
return mans, applied
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def rebuild_sha256(cache: Path, seed: str, secret: str | None, exclusions: dict[str, set[str]] | None,
|
|
504
|
+
rules: list[re.Pattern] | None = None) -> dict[str, str]:
|
|
505
|
+
"""{'single': sha256, 'load': sha256} of the sealed sets the given secret selects (nothing written)."""
|
|
506
|
+
cands, _ = candidates(cache, seed, exclusions, rules)
|
|
507
|
+
sets = select(cands, "sealed", seed, secret)
|
|
508
|
+
return {role: hashlib.sha256(jsonl_bytes(items)).hexdigest() for role, items in sets.items()}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def write_sums(out_dir: Path, manifests: list[dict]) -> Path:
|
|
512
|
+
path = out_dir / "SHA256SUMS"
|
|
513
|
+
lines = [f"{m['sha256']} {m['name']}.jsonl\n" for m in manifests]
|
|
514
|
+
for m in manifests:
|
|
515
|
+
mp = out_dir / f"{m['name']}.manifest.json"
|
|
516
|
+
lines.append(f"{sha256_file(mp)} {mp.name}\n")
|
|
517
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
518
|
+
return path
|
|
Binary file
|