ffbpe 0.1.8__tar.gz
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.
- ffbpe-0.1.8/.gitattributes +1 -0
- ffbpe-0.1.8/.github/scripts/run-benchmark-suite.sh +27 -0
- ffbpe-0.1.8/.github/scripts/update-benchmark-comment.cjs +633 -0
- ffbpe-0.1.8/.github/scripts/update-benchmark-comment.test.cjs +559 -0
- ffbpe-0.1.8/.github/scripts/validate-benchmark-reports.sh +81 -0
- ffbpe-0.1.8/.github/scripts/validate-benchmark-reports.test.sh +122 -0
- ffbpe-0.1.8/.github/workflows/bench-comment.yml +43 -0
- ffbpe-0.1.8/.github/workflows/bench.yml +115 -0
- ffbpe-0.1.8/.github/workflows/ci.yml +77 -0
- ffbpe-0.1.8/.github/workflows/release.yml +56 -0
- ffbpe-0.1.8/.github/workflows/wheels.yml +111 -0
- ffbpe-0.1.8/.gitignore +15 -0
- ffbpe-0.1.8/BENCHMARKS.md +148 -0
- ffbpe-0.1.8/Cargo.lock +2885 -0
- ffbpe-0.1.8/Cargo.toml +101 -0
- ffbpe-0.1.8/LICENSE +21 -0
- ffbpe-0.1.8/PKG-INFO +358 -0
- ffbpe-0.1.8/README.md +346 -0
- ffbpe-0.1.8/benches/bpe.rs +63 -0
- ffbpe-0.1.8/benches/regression/codec.rs +1155 -0
- ffbpe-0.1.8/benches/regression/common/config.rs +44 -0
- ffbpe-0.1.8/benches/regression/common/environment.rs +184 -0
- ffbpe-0.1.8/benches/regression/common/fingerprint.rs +284 -0
- ffbpe-0.1.8/benches/regression/common/mod.rs +7 -0
- ffbpe-0.1.8/benches/regression/common/process.rs +178 -0
- ffbpe-0.1.8/benches/regression/common/report.rs +30 -0
- ffbpe-0.1.8/benches/regression/common/rss.rs +183 -0
- ffbpe-0.1.8/benches/regression/common/util.rs +144 -0
- ffbpe-0.1.8/benches/regression/config/1gib.yml +29 -0
- ffbpe-0.1.8/benches/regression/config/64mib.yml +29 -0
- ffbpe-0.1.8/benches/regression/config/smoke.yml +102 -0
- ffbpe-0.1.8/benches/regression/main.rs +86 -0
- ffbpe-0.1.8/benches/regression/pretokenizer.rs +931 -0
- ffbpe-0.1.8/benches/regression/suite.rs +922 -0
- ffbpe-0.1.8/benches/regression/trainer.rs +1379 -0
- ffbpe-0.1.8/benchmarks/README.md +268 -0
- ffbpe-0.1.8/benchmarks/common.py +290 -0
- ffbpe-0.1.8/benchmarks/compare_hf_training.py +455 -0
- ffbpe-0.1.8/benchmarks/compare_tiktoken.py +150 -0
- ffbpe-0.1.8/benchmarks/count_parquet_source.py +245 -0
- ffbpe-0.1.8/benchmarks/create_fineweb2_sample.py +116 -0
- ffbpe-0.1.8/benchmarks/create_golden_model.py +121 -0
- ffbpe-0.1.8/benchmarks/profile_pretokenizer.py +139 -0
- ffbpe-0.1.8/benchmarks/profile_trainer.py +132 -0
- ffbpe-0.1.8/benchmarks/profile_training_core.py +215 -0
- ffbpe-0.1.8/benchmarks/unicode_bigram_split.py +270 -0
- ffbpe-0.1.8/download.py +67 -0
- ffbpe-0.1.8/examples/profile_perf.rs +148 -0
- ffbpe-0.1.8/examples/quickstart.py +22 -0
- ffbpe-0.1.8/examples/quickstart.rs +20 -0
- ffbpe-0.1.8/fixtures/default_special_tokens.txt +1 -0
- ffbpe-0.1.8/pyproject.toml +35 -0
- ffbpe-0.1.8/python/ffbpe/__init__.py +40 -0
- ffbpe-0.1.8/python/ffbpe/_lib.pyi +115 -0
- ffbpe-0.1.8/python/ffbpe/_serialization.py +86 -0
- ffbpe-0.1.8/python/ffbpe/encoder.py +184 -0
- ffbpe-0.1.8/python/ffbpe/model.py +163 -0
- ffbpe-0.1.8/python/ffbpe/pretokenizer.py +120 -0
- ffbpe-0.1.8/python/ffbpe/py.typed +0 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken/__init__.py +16 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken/core.py +13 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken/load.py +57 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken/model.py +35 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken/registry.py +31 -0
- ffbpe-0.1.8/python/ffbpe/tiktoken_compat.py +418 -0
- ffbpe-0.1.8/python/ffbpe/trainer.py +207 -0
- ffbpe-0.1.8/python/ffbpe/training.py +43 -0
- ffbpe-0.1.8/src/_metrics.rs +217 -0
- ffbpe-0.1.8/src/bigram.rs +169 -0
- ffbpe-0.1.8/src/bpe/encoder.rs +1509 -0
- ffbpe-0.1.8/src/bpe/mod.rs +518 -0
- ffbpe-0.1.8/src/bpe/model.rs +212 -0
- ffbpe-0.1.8/src/bpe/pair.rs +548 -0
- ffbpe-0.1.8/src/bpe/trainer.rs +3073 -0
- ffbpe-0.1.8/src/bpe/utils.rs +355 -0
- ffbpe-0.1.8/src/counter.rs +425 -0
- ffbpe-0.1.8/src/lib.rs +53 -0
- ffbpe-0.1.8/src/main.rs +630 -0
- ffbpe-0.1.8/src/pretokenizer.rs +1321 -0
- ffbpe-0.1.8/src/py.rs +1201 -0
- ffbpe-0.1.8/src/spec/gpt2.rs +143 -0
- ffbpe-0.1.8/src/spec/mod.rs +24 -0
- ffbpe-0.1.8/src/spec/unitoken.rs +244 -0
- ffbpe-0.1.8/src/traits.rs +126 -0
- ffbpe-0.1.8/test.py +51 -0
- ffbpe-0.1.8/tests/regression_report.rs +342 -0
- ffbpe-0.1.8/tests/test_benchmark_manifest.py +66 -0
- ffbpe-0.1.8/tests/test_hf_training_parity.py +100 -0
- ffbpe-0.1.8/tests/test_python_api.py +798 -0
- ffbpe-0.1.8/tests/test_tiktoken_compat.py +186 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fixtures/*.txt binary
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
if [[ "$#" -ne 3 ]]; then
|
|
6
|
+
echo "usage: $0 <checkout> <output-directory> <suite-config>" >&2
|
|
7
|
+
exit 2
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
checkout=$1
|
|
11
|
+
output_dir=$2
|
|
12
|
+
suite_config=$3
|
|
13
|
+
|
|
14
|
+
mkdir -p "$output_dir"
|
|
15
|
+
cd "$checkout"
|
|
16
|
+
|
|
17
|
+
# Keep base/head ordering from turning fixture page-cache state into a PR delta.
|
|
18
|
+
find fixtures -maxdepth 1 -type f -exec sha256sum {} + >/dev/null
|
|
19
|
+
|
|
20
|
+
cargo bench --bench regression --no-run
|
|
21
|
+
|
|
22
|
+
# Each revision consumes its own config. The report renderer treats cases that
|
|
23
|
+
# exist on only one side as missing, which lets benchmark coverage evolve
|
|
24
|
+
# without requiring an older revision to understand a newer schema.
|
|
25
|
+
cargo bench --bench regression -- suite \
|
|
26
|
+
--config "$suite_config" \
|
|
27
|
+
--output-dir "$output_dir"
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const MARKER = '<!-- unitoken-benchmark-report -->';
|
|
5
|
+
const MAX_REPORT_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
const TRAINER_LABELS = new Map([
|
|
7
|
+
['smoke_en_byte_v300', 'English byte, vocab 300'],
|
|
8
|
+
['smoke_en_byte_v1000', 'English byte, vocab 1k'],
|
|
9
|
+
['smoke_zh_unicode_v300', 'Chinese Unicode, vocab 300'],
|
|
10
|
+
['smoke_zh_unicode_v1000', 'Chinese Unicode, vocab 1k'],
|
|
11
|
+
['smoke_zh_unicode_bbpe_r90_v1000', 'Chinese Unicode BBPE, vocab 1k'],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function finiteNumber(value) {
|
|
19
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
20
|
+
? value
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function average(values) {
|
|
25
|
+
const valid = values.map(finiteNumber).filter((value) => value !== null);
|
|
26
|
+
if (valid.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return valid.reduce((sum, value) => sum + value, 0) / valid.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadReport(resultsDir, relativePath, contract, errors, optional) {
|
|
33
|
+
const reportPath = path.join(resultsDir, relativePath);
|
|
34
|
+
try {
|
|
35
|
+
const stat = fs.lstatSync(reportPath);
|
|
36
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_REPORT_BYTES) {
|
|
37
|
+
throw new Error('invalid report file');
|
|
38
|
+
}
|
|
39
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
40
|
+
if (
|
|
41
|
+
!isRecord(report)
|
|
42
|
+
|| report.schema_version !== 1
|
|
43
|
+
|| report.contract !== contract
|
|
44
|
+
|| !isRecord(report.gates)
|
|
45
|
+
|| typeof report.gates.passed !== 'boolean'
|
|
46
|
+
|| !Array.isArray(report.samples)
|
|
47
|
+
) {
|
|
48
|
+
throw new Error('invalid report contract');
|
|
49
|
+
}
|
|
50
|
+
return { status: 'present', report };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (optional && error?.code === 'ENOENT') {
|
|
53
|
+
return { status: 'absent', report: null };
|
|
54
|
+
}
|
|
55
|
+
errors.push(relativePath);
|
|
56
|
+
return { status: 'invalid', report: null };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readReport(resultsDir, relativePath, contract, errors) {
|
|
61
|
+
return loadReport(resultsDir, relativePath, contract, errors, false).report;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readOptionalReport(resultsDir, relativePath, contract, errors) {
|
|
65
|
+
return loadReport(resultsDir, relativePath, contract, errors, true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readMetadata(resultsDir) {
|
|
69
|
+
const metadataPath = path.join(resultsDir, 'metadata.json');
|
|
70
|
+
const stat = fs.lstatSync(metadataPath);
|
|
71
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) {
|
|
72
|
+
throw new Error('invalid benchmark metadata file');
|
|
73
|
+
}
|
|
74
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
75
|
+
if (
|
|
76
|
+
!isRecord(metadata)
|
|
77
|
+
|| metadata.schema_version !== 1
|
|
78
|
+
|| !Number.isSafeInteger(metadata.pull_request_number)
|
|
79
|
+
|| metadata.pull_request_number <= 0
|
|
80
|
+
|| !/^[0-9a-f]{40}$/.test(metadata.base_sha)
|
|
81
|
+
|| !/^[0-9a-f]{40}$/.test(metadata.head_sha)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('invalid benchmark metadata contract');
|
|
84
|
+
}
|
|
85
|
+
return metadata;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function variantLabel(variant) {
|
|
89
|
+
if (variant?.occurrence_mode === 'exact') {
|
|
90
|
+
return 'exact';
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
variant?.occurrence_mode === 'bounded'
|
|
94
|
+
&& Number.isSafeInteger(variant.hot_pair_window_size)
|
|
95
|
+
&& variant.hot_pair_window_size > 0
|
|
96
|
+
) {
|
|
97
|
+
return `k${variant.hot_pair_window_size}`;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function stableValue(value) {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return value.map(stableValue);
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(value)) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return Object.fromEntries(
|
|
110
|
+
Object.keys(value)
|
|
111
|
+
.sort()
|
|
112
|
+
.map((key) => [key, stableValue(value[key])]),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function trainerWorkload(sample) {
|
|
117
|
+
const requestCase = isRecord(sample?.request?.case) ? sample.request.case : {};
|
|
118
|
+
const expectedInputSha256 = requestCase.expected_input_sha256;
|
|
119
|
+
const normalizedCase = {
|
|
120
|
+
...requestCase,
|
|
121
|
+
bbpe_fallback: requestCase.bbpe_fallback ?? false,
|
|
122
|
+
primary_vocab_ratio: requestCase.primary_vocab_ratio ?? 0.9,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Checkout-local paths differ between the base and candidate worktrees. The
|
|
126
|
+
// expected and measured input fingerprints identify the corpus instead.
|
|
127
|
+
delete normalizedCase.words_path;
|
|
128
|
+
// Golden inputs and outputs are assertions about the work, not its settings.
|
|
129
|
+
delete normalizedCase.expected_input_sha256;
|
|
130
|
+
delete normalizedCase.expected_model_sha256;
|
|
131
|
+
|
|
132
|
+
return JSON.stringify(stableValue({
|
|
133
|
+
case: normalizedCase,
|
|
134
|
+
variant: sample?.request?.variant,
|
|
135
|
+
input_sha256: sample?.measurement?.input?.sha256
|
|
136
|
+
?? expectedInputSha256
|
|
137
|
+
?? null,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function trainerRows(report) {
|
|
142
|
+
const rows = new Map();
|
|
143
|
+
for (const sample of report?.samples ?? []) {
|
|
144
|
+
const caseName = sample?.request?.case?.name;
|
|
145
|
+
const variant = variantLabel(sample?.request?.variant);
|
|
146
|
+
if (typeof caseName !== 'string' || caseName.length === 0 || variant === null) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const key = JSON.stringify([caseName, variant]);
|
|
150
|
+
let row = rows.get(key);
|
|
151
|
+
if (!row) {
|
|
152
|
+
row = {
|
|
153
|
+
caseName,
|
|
154
|
+
variant,
|
|
155
|
+
times: [],
|
|
156
|
+
rssValues: [],
|
|
157
|
+
workloads: new Set(),
|
|
158
|
+
failed: report.gates.passed === false,
|
|
159
|
+
};
|
|
160
|
+
rows.set(key, row);
|
|
161
|
+
}
|
|
162
|
+
row.failed ||= sample?.status === 'failed' || sample?.error != null;
|
|
163
|
+
row.times.push(sample?.measurement?.timing?.core_training_ns);
|
|
164
|
+
row.rssValues.push(
|
|
165
|
+
sample?.measurement?.memory?.process_peak_rss_through_training_bytes,
|
|
166
|
+
);
|
|
167
|
+
row.workloads.add(trainerWorkload(sample));
|
|
168
|
+
}
|
|
169
|
+
for (const row of rows.values()) {
|
|
170
|
+
row.time = average(row.times);
|
|
171
|
+
row.rss = average(row.rssValues);
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function unionTrainerRows(
|
|
177
|
+
baseline,
|
|
178
|
+
candidate,
|
|
179
|
+
baselineAvailable,
|
|
180
|
+
candidateAvailable,
|
|
181
|
+
) {
|
|
182
|
+
const rows = new Map();
|
|
183
|
+
for (const [key, row] of baseline) {
|
|
184
|
+
rows.set(key, {
|
|
185
|
+
caseName: row.caseName,
|
|
186
|
+
variant: row.variant,
|
|
187
|
+
baseline: row,
|
|
188
|
+
candidate: null,
|
|
189
|
+
baselineAvailable,
|
|
190
|
+
candidateAvailable,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
for (const [key, row] of candidate) {
|
|
194
|
+
const existing = rows.get(key);
|
|
195
|
+
if (existing) {
|
|
196
|
+
existing.candidate = row;
|
|
197
|
+
} else {
|
|
198
|
+
rows.set(key, {
|
|
199
|
+
caseName: row.caseName,
|
|
200
|
+
variant: row.variant,
|
|
201
|
+
baseline: null,
|
|
202
|
+
candidate: row,
|
|
203
|
+
baselineAvailable,
|
|
204
|
+
candidateAvailable,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...rows.values()];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sameWorkloads(baseline, candidate) {
|
|
212
|
+
if (baseline.workloads.size !== candidate.workloads.size) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return [...baseline.workloads].every((workload) => candidate.workloads.has(workload));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pretokenizerValues(report) {
|
|
219
|
+
const values = new Map();
|
|
220
|
+
const samples = report?.samples ?? [];
|
|
221
|
+
values.set('bigram', average(
|
|
222
|
+
samples.map((sample) => sample?.measurement?.timing?.bigram_pass_ns),
|
|
223
|
+
));
|
|
224
|
+
values.set('word', average(
|
|
225
|
+
samples.map((sample) => sample?.measurement?.timing?.word_pass_ns),
|
|
226
|
+
));
|
|
227
|
+
values.set('total', average(
|
|
228
|
+
samples.map((sample) => sample?.measurement?.timing?.core_pretokenizer_ns),
|
|
229
|
+
));
|
|
230
|
+
values.set('rss', average(
|
|
231
|
+
samples.map(
|
|
232
|
+
(sample) => sample?.measurement?.memory?.process_peak_rss_through_core_bytes,
|
|
233
|
+
),
|
|
234
|
+
));
|
|
235
|
+
return values;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function codecValues(report) {
|
|
239
|
+
const values = new Map();
|
|
240
|
+
const samples = report?.samples ?? [];
|
|
241
|
+
values.set('encode', average(
|
|
242
|
+
samples.map((sample) => sample?.encode?.timing?.encode_ns),
|
|
243
|
+
));
|
|
244
|
+
values.set('decode', average(
|
|
245
|
+
samples.map((sample) => sample?.decode?.timing?.decode_ns),
|
|
246
|
+
));
|
|
247
|
+
values.set('encode_rss', average(
|
|
248
|
+
samples.map(
|
|
249
|
+
(sample) => sample?.encode?.memory?.process_peak_rss_through_phase_bytes,
|
|
250
|
+
),
|
|
251
|
+
));
|
|
252
|
+
values.set('decode_rss', average(
|
|
253
|
+
samples.map(
|
|
254
|
+
(sample) => sample?.decode?.memory?.process_peak_rss_through_phase_bytes,
|
|
255
|
+
),
|
|
256
|
+
));
|
|
257
|
+
return values;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function codecWorkload(report) {
|
|
261
|
+
const config = { ...(report?.config ?? {}) };
|
|
262
|
+
for (const key of Object.keys(config)) {
|
|
263
|
+
if (key === 'name' || key.endsWith('_path') || key.startsWith('expected_')) {
|
|
264
|
+
delete config[key];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const measurements = [...new Set(
|
|
268
|
+
(report?.samples ?? []).map((sample) => JSON.stringify(stableValue({
|
|
269
|
+
input: sample?.encode?.input ?? null,
|
|
270
|
+
model: sample?.encode?.model ?? null,
|
|
271
|
+
}))),
|
|
272
|
+
)].sort();
|
|
273
|
+
return JSON.stringify(stableValue({ config, measurements }));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatMilliseconds(value) {
|
|
277
|
+
return value === null ? 'n/a' : `${(value / 1_000_000).toFixed(2)} ms`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function formatMebibytes(value) {
|
|
281
|
+
return value === null ? 'n/a' : `${(value / 1024 / 1024).toFixed(1)} MiB`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatDelta(baseline, candidate) {
|
|
285
|
+
if (baseline === null || candidate === null || baseline === 0) {
|
|
286
|
+
return 'n/a';
|
|
287
|
+
}
|
|
288
|
+
const delta = ((candidate - baseline) / baseline) * 100;
|
|
289
|
+
return `${delta >= 0 ? '+' : ''}${delta.toFixed(1)}%`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tableRow(label, baseline, candidate, formatter) {
|
|
293
|
+
return `| ${label} | ${formatter(baseline)} | ${formatter(candidate)} | ${formatDelta(baseline, candidate)} |`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function escapeTableCell(value) {
|
|
297
|
+
return value.replaceAll('|', '\\|').replace(/[\r\n]+/g, ' ');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function trainerCellValue(row, reportAvailable, metric, formatter) {
|
|
301
|
+
if (!reportAvailable) {
|
|
302
|
+
return 'unavailable';
|
|
303
|
+
}
|
|
304
|
+
if (!row) {
|
|
305
|
+
return 'missing';
|
|
306
|
+
}
|
|
307
|
+
return row.failed ? 'failed' : formatter(row[metric]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function trainerTableRow(row, metric, formatter) {
|
|
311
|
+
const baseline = row.baseline;
|
|
312
|
+
const candidate = row.candidate;
|
|
313
|
+
const baselineValue = trainerCellValue(
|
|
314
|
+
baseline,
|
|
315
|
+
row.baselineAvailable,
|
|
316
|
+
metric,
|
|
317
|
+
formatter,
|
|
318
|
+
);
|
|
319
|
+
const candidateValue = trainerCellValue(
|
|
320
|
+
candidate,
|
|
321
|
+
row.candidateAvailable,
|
|
322
|
+
metric,
|
|
323
|
+
formatter,
|
|
324
|
+
);
|
|
325
|
+
let delta = 'n/a';
|
|
326
|
+
if (baseline && candidate && !baseline.failed && !candidate.failed) {
|
|
327
|
+
delta = sameWorkloads(baseline, candidate)
|
|
328
|
+
? formatDelta(baseline[metric], candidate[metric])
|
|
329
|
+
: 'changed';
|
|
330
|
+
}
|
|
331
|
+
const caseLabel = TRAINER_LABELS.get(row.caseName) ?? row.caseName;
|
|
332
|
+
const label = escapeTableCell(`Trainer — ${caseLabel} (${row.variant})`);
|
|
333
|
+
return `| ${label} | ${baselineValue} | ${candidateValue} | ${delta} |`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function optionalReportCell(state, values, metric, formatter) {
|
|
337
|
+
if (state.status === 'invalid') {
|
|
338
|
+
return 'unavailable';
|
|
339
|
+
}
|
|
340
|
+
if (state.status === 'absent') {
|
|
341
|
+
return 'missing';
|
|
342
|
+
}
|
|
343
|
+
if (state.report.gates.passed !== true) {
|
|
344
|
+
return 'failed';
|
|
345
|
+
}
|
|
346
|
+
return formatter(values.get(metric) ?? null);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function optionalCodecTableRow(
|
|
350
|
+
label,
|
|
351
|
+
baseline,
|
|
352
|
+
candidate,
|
|
353
|
+
baselineValues,
|
|
354
|
+
candidateValues,
|
|
355
|
+
metric,
|
|
356
|
+
formatter,
|
|
357
|
+
) {
|
|
358
|
+
const baselineValue = baselineValues.get(metric) ?? null;
|
|
359
|
+
const candidateValue = candidateValues.get(metric) ?? null;
|
|
360
|
+
let delta = 'n/a';
|
|
361
|
+
if (
|
|
362
|
+
baseline.status === 'present'
|
|
363
|
+
&& baseline.report.gates.passed === true
|
|
364
|
+
&& candidate.status === 'present'
|
|
365
|
+
&& candidate.report.gates.passed === true
|
|
366
|
+
) {
|
|
367
|
+
delta = codecWorkload(baseline.report) === codecWorkload(candidate.report)
|
|
368
|
+
? formatDelta(baselineValue, candidateValue)
|
|
369
|
+
: 'changed';
|
|
370
|
+
}
|
|
371
|
+
return `| ${label} | ${optionalReportCell(baseline, baselineValues, metric, formatter)} | ${optionalReportCell(candidate, candidateValues, metric, formatter)} | ${delta} |`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function reportSet(resultsDir, side, errors) {
|
|
375
|
+
const prefix = `${side}/`;
|
|
376
|
+
return {
|
|
377
|
+
trainer: readReport(
|
|
378
|
+
resultsDir,
|
|
379
|
+
`${prefix}trainer.json`,
|
|
380
|
+
'unitoken_trainer_regression_v1',
|
|
381
|
+
errors,
|
|
382
|
+
),
|
|
383
|
+
pretokenizer: readReport(
|
|
384
|
+
resultsDir,
|
|
385
|
+
`${prefix}pretokenizer.json`,
|
|
386
|
+
'unitoken_pretokenizer_regression_v1',
|
|
387
|
+
errors,
|
|
388
|
+
),
|
|
389
|
+
byteCodec: readReport(
|
|
390
|
+
resultsDir,
|
|
391
|
+
`${prefix}codec-byte.json`,
|
|
392
|
+
'unitoken_codec_regression_v1',
|
|
393
|
+
errors,
|
|
394
|
+
),
|
|
395
|
+
unicodeCodec: readReport(
|
|
396
|
+
resultsDir,
|
|
397
|
+
`${prefix}codec-unicode.json`,
|
|
398
|
+
'unitoken_codec_regression_v1',
|
|
399
|
+
errors,
|
|
400
|
+
),
|
|
401
|
+
bbpeUnicodeCodec: readOptionalReport(
|
|
402
|
+
resultsDir,
|
|
403
|
+
`${prefix}codec-unicode-bbpe.json`,
|
|
404
|
+
'unitoken_codec_regression_v1',
|
|
405
|
+
errors,
|
|
406
|
+
),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function buildComment({ resultsDir, conclusion, baseSha, headSha, runUrl }) {
|
|
411
|
+
const errors = [];
|
|
412
|
+
const baseline = reportSet(resultsDir, 'baseline', errors);
|
|
413
|
+
const candidate = reportSet(resultsDir, 'candidate', errors);
|
|
414
|
+
const trainerRowsToRender = unionTrainerRows(
|
|
415
|
+
trainerRows(baseline.trainer),
|
|
416
|
+
trainerRows(candidate.trainer),
|
|
417
|
+
baseline.trainer !== null,
|
|
418
|
+
candidate.trainer !== null,
|
|
419
|
+
);
|
|
420
|
+
const reports = [
|
|
421
|
+
baseline.trainer,
|
|
422
|
+
baseline.pretokenizer,
|
|
423
|
+
baseline.byteCodec,
|
|
424
|
+
baseline.unicodeCodec,
|
|
425
|
+
baseline.bbpeUnicodeCodec.report,
|
|
426
|
+
candidate.trainer,
|
|
427
|
+
candidate.pretokenizer,
|
|
428
|
+
candidate.byteCodec,
|
|
429
|
+
candidate.unicodeCodec,
|
|
430
|
+
candidate.bbpeUnicodeCodec.report,
|
|
431
|
+
].filter((report) => report !== null);
|
|
432
|
+
const bbpeCodecRegression = baseline.bbpeUnicodeCodec.status === 'present'
|
|
433
|
+
&& candidate.bbpeUnicodeCodec.status === 'absent';
|
|
434
|
+
const passed = conclusion === 'success'
|
|
435
|
+
&& errors.length === 0
|
|
436
|
+
&& !bbpeCodecRegression
|
|
437
|
+
&& reports.every((report) => report?.gates?.passed === true)
|
|
438
|
+
&& trainerRowsToRender.every(
|
|
439
|
+
(row) => !row.baseline?.failed && !row.candidate?.failed,
|
|
440
|
+
);
|
|
441
|
+
const basePretokenizer = pretokenizerValues(baseline.pretokenizer);
|
|
442
|
+
const headPretokenizer = pretokenizerValues(candidate.pretokenizer);
|
|
443
|
+
const baseByteCodec = codecValues(baseline.byteCodec);
|
|
444
|
+
const headByteCodec = codecValues(candidate.byteCodec);
|
|
445
|
+
const baseUnicodeCodec = codecValues(baseline.unicodeCodec);
|
|
446
|
+
const headUnicodeCodec = codecValues(candidate.unicodeCodec);
|
|
447
|
+
const baseBbpeUnicodeCodec = codecValues(baseline.bbpeUnicodeCodec.report);
|
|
448
|
+
const headBbpeUnicodeCodec = codecValues(candidate.bbpeUnicodeCodec.report);
|
|
449
|
+
const renderBbpeUnicodeCodec = baseline.bbpeUnicodeCodec.status !== 'absent'
|
|
450
|
+
|| candidate.bbpeUnicodeCodec.status !== 'absent';
|
|
451
|
+
const lines = [
|
|
452
|
+
MARKER,
|
|
453
|
+
'## Benchmark report',
|
|
454
|
+
'',
|
|
455
|
+
passed
|
|
456
|
+
? '✅ All base and PR correctness gates passed.'
|
|
457
|
+
: '❌ The benchmark run or at least one correctness gate failed.',
|
|
458
|
+
'',
|
|
459
|
+
`Compared \`${baseSha.slice(0, 7)}\` → \`${headSha.slice(0, 7)}\` sequentially on the same runner. Timing deltas are informational.`,
|
|
460
|
+
'',
|
|
461
|
+
'Trainer and optional-codec cells marked `missing` are absent cases or reports; `unavailable` means a required report is missing or a report is invalid; `failed` means that revision failed its report or case; `changed` means the workloads are not comparable.',
|
|
462
|
+
'',
|
|
463
|
+
'| Benchmark | Base | PR | Δ |',
|
|
464
|
+
'| --- | ---: | ---: | ---: |',
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
for (const row of trainerRowsToRender) {
|
|
468
|
+
lines.push(trainerTableRow(row, 'time', formatMilliseconds));
|
|
469
|
+
}
|
|
470
|
+
for (const [label, key] of [
|
|
471
|
+
['Pretokenizer — bigram pass', 'bigram'],
|
|
472
|
+
['Pretokenizer — word pass', 'word'],
|
|
473
|
+
['Pretokenizer — total', 'total'],
|
|
474
|
+
]) {
|
|
475
|
+
lines.push(tableRow(
|
|
476
|
+
label,
|
|
477
|
+
basePretokenizer.get(key) ?? null,
|
|
478
|
+
headPretokenizer.get(key) ?? null,
|
|
479
|
+
formatMilliseconds,
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
for (const [label, values, key] of [
|
|
483
|
+
['Codec — byte encode', [baseByteCodec, headByteCodec], 'encode'],
|
|
484
|
+
['Codec — byte decode', [baseByteCodec, headByteCodec], 'decode'],
|
|
485
|
+
['Codec — Unicode encode', [baseUnicodeCodec, headUnicodeCodec], 'encode'],
|
|
486
|
+
['Codec — Unicode decode', [baseUnicodeCodec, headUnicodeCodec], 'decode'],
|
|
487
|
+
]) {
|
|
488
|
+
lines.push(tableRow(
|
|
489
|
+
label,
|
|
490
|
+
values[0].get(key) ?? null,
|
|
491
|
+
values[1].get(key) ?? null,
|
|
492
|
+
formatMilliseconds,
|
|
493
|
+
));
|
|
494
|
+
}
|
|
495
|
+
if (renderBbpeUnicodeCodec) {
|
|
496
|
+
for (const [label, key] of [
|
|
497
|
+
['Codec — Unicode BBPE encode, vocab 1k', 'encode'],
|
|
498
|
+
['Codec — Unicode BBPE decode, vocab 1k', 'decode'],
|
|
499
|
+
]) {
|
|
500
|
+
lines.push(optionalCodecTableRow(
|
|
501
|
+
label,
|
|
502
|
+
baseline.bbpeUnicodeCodec,
|
|
503
|
+
candidate.bbpeUnicodeCodec,
|
|
504
|
+
baseBbpeUnicodeCodec,
|
|
505
|
+
headBbpeUnicodeCodec,
|
|
506
|
+
key,
|
|
507
|
+
formatMilliseconds,
|
|
508
|
+
));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
lines.push('', '<details>', '<summary>Peak RSS</summary>', '');
|
|
513
|
+
lines.push('| Benchmark | Base | PR | Δ |');
|
|
514
|
+
lines.push('| --- | ---: | ---: | ---: |');
|
|
515
|
+
for (const row of trainerRowsToRender) {
|
|
516
|
+
lines.push(trainerTableRow(row, 'rss', formatMebibytes));
|
|
517
|
+
}
|
|
518
|
+
lines.push(tableRow(
|
|
519
|
+
'Pretokenizer',
|
|
520
|
+
basePretokenizer.get('rss') ?? null,
|
|
521
|
+
headPretokenizer.get('rss') ?? null,
|
|
522
|
+
formatMebibytes,
|
|
523
|
+
));
|
|
524
|
+
for (const [label, values, key] of [
|
|
525
|
+
['Codec — byte encode', [baseByteCodec, headByteCodec], 'encode_rss'],
|
|
526
|
+
['Codec — byte decode', [baseByteCodec, headByteCodec], 'decode_rss'],
|
|
527
|
+
['Codec — Unicode encode', [baseUnicodeCodec, headUnicodeCodec], 'encode_rss'],
|
|
528
|
+
['Codec — Unicode decode', [baseUnicodeCodec, headUnicodeCodec], 'decode_rss'],
|
|
529
|
+
]) {
|
|
530
|
+
lines.push(tableRow(
|
|
531
|
+
label,
|
|
532
|
+
values[0].get(key) ?? null,
|
|
533
|
+
values[1].get(key) ?? null,
|
|
534
|
+
formatMebibytes,
|
|
535
|
+
));
|
|
536
|
+
}
|
|
537
|
+
if (renderBbpeUnicodeCodec) {
|
|
538
|
+
for (const [label, key] of [
|
|
539
|
+
['Codec — Unicode BBPE encode, vocab 1k', 'encode_rss'],
|
|
540
|
+
['Codec — Unicode BBPE decode, vocab 1k', 'decode_rss'],
|
|
541
|
+
]) {
|
|
542
|
+
lines.push(optionalCodecTableRow(
|
|
543
|
+
label,
|
|
544
|
+
baseline.bbpeUnicodeCodec,
|
|
545
|
+
candidate.bbpeUnicodeCodec,
|
|
546
|
+
baseBbpeUnicodeCodec,
|
|
547
|
+
headBbpeUnicodeCodec,
|
|
548
|
+
key,
|
|
549
|
+
formatMebibytes,
|
|
550
|
+
));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
lines.push('', '</details>', '');
|
|
554
|
+
if (bbpeCodecRegression) {
|
|
555
|
+
lines.push(
|
|
556
|
+
'Optional benchmark regression: `candidate/codec-unicode-bbpe.json` is absent while the base report is present.',
|
|
557
|
+
'',
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (errors.length > 0) {
|
|
561
|
+
lines.push(`Missing or invalid reports: ${errors.map((name) => `\`${name}\``).join(', ')}.`, '');
|
|
562
|
+
}
|
|
563
|
+
lines.push(`[Open benchmark run](${runUrl})`);
|
|
564
|
+
return lines.join('\n');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function updateBenchmarkComment({ github, context, core }) {
|
|
568
|
+
const workflowRun = context.payload.workflow_run;
|
|
569
|
+
const metadata = readMetadata(process.env.BENCHMARK_RESULTS_DIR);
|
|
570
|
+
const pullRequests = workflowRun?.pull_requests ?? [];
|
|
571
|
+
if (
|
|
572
|
+
pullRequests.length > 1
|
|
573
|
+
|| (pullRequests.length === 1 && pullRequests[0].number !== metadata.pull_request_number)
|
|
574
|
+
) {
|
|
575
|
+
core.info('Benchmark metadata does not match the triggering pull request; skipping comment.');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const pullNumber = metadata.pull_request_number;
|
|
580
|
+
const owner = context.repo.owner;
|
|
581
|
+
const repo = context.repo.repo;
|
|
582
|
+
const { data: pullRequest } = await github.rest.pulls.get({
|
|
583
|
+
owner,
|
|
584
|
+
repo,
|
|
585
|
+
pull_number: pullNumber,
|
|
586
|
+
});
|
|
587
|
+
const headRepository = workflowRun?.head_repository?.full_name;
|
|
588
|
+
if (
|
|
589
|
+
pullRequest.head.sha !== metadata.head_sha
|
|
590
|
+
|| pullRequest.head.ref !== workflowRun?.head_branch
|
|
591
|
+
|| pullRequest.head.repo?.full_name !== headRepository
|
|
592
|
+
) {
|
|
593
|
+
core.info('The benchmark origin or PR head no longer matches; skipping stale results.');
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const runId = process.env.WORKFLOW_RUN_ID;
|
|
598
|
+
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${runId}`;
|
|
599
|
+
const body = buildComment({
|
|
600
|
+
resultsDir: process.env.BENCHMARK_RESULTS_DIR,
|
|
601
|
+
conclusion: process.env.WORKFLOW_CONCLUSION,
|
|
602
|
+
baseSha: metadata.base_sha,
|
|
603
|
+
headSha: metadata.head_sha,
|
|
604
|
+
runUrl,
|
|
605
|
+
});
|
|
606
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
607
|
+
owner,
|
|
608
|
+
repo,
|
|
609
|
+
issue_number: pullNumber,
|
|
610
|
+
per_page: 100,
|
|
611
|
+
});
|
|
612
|
+
const existing = comments.find(
|
|
613
|
+
(comment) => comment.user?.type === 'Bot' && comment.body?.includes(MARKER),
|
|
614
|
+
);
|
|
615
|
+
if (existing) {
|
|
616
|
+
await github.rest.issues.updateComment({
|
|
617
|
+
owner,
|
|
618
|
+
repo,
|
|
619
|
+
comment_id: existing.id,
|
|
620
|
+
body,
|
|
621
|
+
});
|
|
622
|
+
} else {
|
|
623
|
+
await github.rest.issues.createComment({
|
|
624
|
+
owner,
|
|
625
|
+
repo,
|
|
626
|
+
issue_number: pullNumber,
|
|
627
|
+
body,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
module.exports = updateBenchmarkComment;
|
|
633
|
+
module.exports.buildComment = buildComment;
|