@tuned-tensor/cli 0.21.0 → 0.22.0
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.
- package/README.md +83 -0
- package/dist/{chunk-PVLX7LJI.js → chunk-4SUXBHIY.js} +1319 -229
- package/dist/chunk-4SUXBHIY.js.map +1 -0
- package/dist/index.js +237 -37
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +107 -44
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +38 -0
- package/docs/local-runtime/aws-gpu.md +2 -0
- package/docs/spec-workflow.md +7 -0
- package/package.json +1 -1
- package/training/adapter/src/hub_download.py +384 -0
- package/training/adapter/src/prefetch.py +20 -31
- package/training/adapter/src/prefetch_dataset.py +116 -0
- package/training/adapter/src/serve.py +5 -1
- package/dist/chunk-PVLX7LJI.js.map +0 -1
|
@@ -1010,11 +1010,34 @@ var foundationHyperparametersSchema = z.object({
|
|
|
1010
1010
|
keep_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
1011
1011
|
log_interval_steps: z.number().int().min(1).max(1e6).optional()
|
|
1012
1012
|
}).strict();
|
|
1013
|
+
var huggingFaceRepoIdSchema = z.string().regex(
|
|
1014
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/,
|
|
1015
|
+
"huggingface.repo must be a Hugging Face dataset id such as org/name"
|
|
1016
|
+
);
|
|
1017
|
+
var datasetColumnsSchema = z.object({
|
|
1018
|
+
input: z.string().min(1),
|
|
1019
|
+
output: z.string().min(1)
|
|
1020
|
+
}).strict();
|
|
1021
|
+
var huggingFaceDatasetSourceSchema = z.object({
|
|
1022
|
+
repo: huggingFaceRepoIdSchema,
|
|
1023
|
+
revision: baseModelRevisionSchema.optional(),
|
|
1024
|
+
/**
|
|
1025
|
+
* Field names for plain JSONL records such as `{"text": ..., "label": ...}`.
|
|
1026
|
+
* Each record becomes a chat row with the spec's compiled system message.
|
|
1027
|
+
* Omit for files that are already TT chat JSONL.
|
|
1028
|
+
*/
|
|
1029
|
+
columns: datasetColumnsSchema.optional()
|
|
1030
|
+
}).strict();
|
|
1031
|
+
function isRepoRelativePath(value) {
|
|
1032
|
+
if (/^(?:[A-Za-z]:|[/\\]|file:)/.test(value)) return false;
|
|
1033
|
+
return value.split(/[/\\]/).every((part) => part !== "" && part !== "." && part !== "..");
|
|
1034
|
+
}
|
|
1013
1035
|
var datasetPrebuiltSchema = z.object({
|
|
1014
1036
|
training: z.string().min(1),
|
|
1015
1037
|
validation: z.string().min(1).optional(),
|
|
1016
1038
|
test: z.string().min(1).optional(),
|
|
1017
|
-
format: datasetFormatSchema.default("chat_jsonl")
|
|
1039
|
+
format: datasetFormatSchema.default("chat_jsonl"),
|
|
1040
|
+
huggingface: huggingFaceDatasetSourceSchema.optional()
|
|
1018
1041
|
}).strict().superRefine((dataset, context) => {
|
|
1019
1042
|
if (!dataset.validation && !dataset.test) {
|
|
1020
1043
|
context.addIssue({
|
|
@@ -1023,7 +1046,57 @@ var datasetPrebuiltSchema = z.object({
|
|
|
1023
1046
|
message: "Provide a distinct validation or test split for held-out evaluation"
|
|
1024
1047
|
});
|
|
1025
1048
|
}
|
|
1049
|
+
if (!dataset.huggingface) return;
|
|
1050
|
+
for (const key of ["training", "validation", "test"]) {
|
|
1051
|
+
const value = dataset[key];
|
|
1052
|
+
if (value === void 0) continue;
|
|
1053
|
+
if (!isRepoRelativePath(value)) {
|
|
1054
|
+
context.addIssue({
|
|
1055
|
+
code: z.ZodIssueCode.custom,
|
|
1056
|
+
path: [key],
|
|
1057
|
+
message: `With dataset_prebuilt.huggingface, ${key} must be a file path inside the dataset repo, such as data/${key}.jsonl`
|
|
1058
|
+
});
|
|
1059
|
+
} else if (!value.toLowerCase().endsWith(".jsonl")) {
|
|
1060
|
+
context.addIssue({
|
|
1061
|
+
code: z.ZodIssueCode.custom,
|
|
1062
|
+
path: [key],
|
|
1063
|
+
message: `${key} must be a chat JSONL (.jsonl) file; Parquet, CSV and other formats are not supported yet`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1026
1067
|
});
|
|
1068
|
+
var datasetSourceFileSchema = z.object({
|
|
1069
|
+
path: z.string().min(1),
|
|
1070
|
+
size_bytes: z.number().int().nonnegative(),
|
|
1071
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
1072
|
+
}).strict();
|
|
1073
|
+
var datasetConversionStatsSchema = z.object({
|
|
1074
|
+
records: z.number().int().nonnegative(),
|
|
1075
|
+
kept: z.number().int().nonnegative(),
|
|
1076
|
+
skipped_missing_fields: z.number().int().nonnegative(),
|
|
1077
|
+
dropped_duplicate_inputs: z.number().int().nonnegative(),
|
|
1078
|
+
dropped_eval_overlap: z.number().int().nonnegative()
|
|
1079
|
+
}).strict();
|
|
1080
|
+
var datasetSourceSchema = z.object({
|
|
1081
|
+
provider: z.literal("huggingface"),
|
|
1082
|
+
repo: huggingFaceRepoIdSchema,
|
|
1083
|
+
requested_revision: baseModelRevisionSchema.nullable(),
|
|
1084
|
+
revision: baseModelRevisionSchema,
|
|
1085
|
+
pinned: z.boolean(),
|
|
1086
|
+
columns: datasetColumnsSchema.nullable(),
|
|
1087
|
+
/** Per-split record accounting when `columns` converted plain JSONL records. */
|
|
1088
|
+
conversion: z.object({
|
|
1089
|
+
training: datasetConversionStatsSchema,
|
|
1090
|
+
validation: datasetConversionStatsSchema.optional(),
|
|
1091
|
+
test: datasetConversionStatsSchema.optional()
|
|
1092
|
+
}).strict().nullable().optional(),
|
|
1093
|
+
snapshot_path: z.string().min(1),
|
|
1094
|
+
files: z.object({
|
|
1095
|
+
training: datasetSourceFileSchema,
|
|
1096
|
+
validation: datasetSourceFileSchema.optional(),
|
|
1097
|
+
test: datasetSourceFileSchema.optional()
|
|
1098
|
+
}).strict()
|
|
1099
|
+
}).strict();
|
|
1027
1100
|
var fineTuneHyperparametersSchema = z.object({
|
|
1028
1101
|
n_epochs: z.number().int().min(1).max(20).default(1),
|
|
1029
1102
|
learning_rate: z.number().positive().optional(),
|
|
@@ -1053,7 +1126,8 @@ var fineTuneRunRequestSchema = z.object({
|
|
|
1053
1126
|
run_number: z.number().int().min(1),
|
|
1054
1127
|
spec_snapshot: specSnapshotSchema,
|
|
1055
1128
|
hyperparameters: fineTuneHyperparametersSchema.default({ n_epochs: 1 }),
|
|
1056
|
-
dataset_prebuilt: datasetPrebuiltSchema.optional()
|
|
1129
|
+
dataset_prebuilt: datasetPrebuiltSchema.optional(),
|
|
1130
|
+
dataset_source: datasetSourceSchema.optional()
|
|
1057
1131
|
}).strict().superRefine((request, context) => {
|
|
1058
1132
|
requirePinnedUncertifiedModel(request, context);
|
|
1059
1133
|
if (request.spec_snapshot.examples.length === 0 && !request.dataset_prebuilt) {
|
|
@@ -1143,6 +1217,55 @@ var trainingReportSchema = z.object({
|
|
|
1143
1217
|
log_uri: z.string(),
|
|
1144
1218
|
command: z.array(z.string()).optional()
|
|
1145
1219
|
}).strict();
|
|
1220
|
+
var runProvenanceSchema = z.object({
|
|
1221
|
+
schema_version: z.literal(1),
|
|
1222
|
+
recorded_at: z.string(),
|
|
1223
|
+
software: z.object({
|
|
1224
|
+
tt_version: z.string(),
|
|
1225
|
+
node_version: z.string(),
|
|
1226
|
+
platform: z.string(),
|
|
1227
|
+
arch: z.string(),
|
|
1228
|
+
runtime_fingerprint: z.string(),
|
|
1229
|
+
python_lock_sha256: z.string().nullable()
|
|
1230
|
+
}).strict(),
|
|
1231
|
+
execution: z.object({
|
|
1232
|
+
target: z.enum(["local", "aws"]),
|
|
1233
|
+
training_backend: z.string(),
|
|
1234
|
+
instance_id: z.string().optional(),
|
|
1235
|
+
region: z.string().optional()
|
|
1236
|
+
}).strict(),
|
|
1237
|
+
spec: z.object({
|
|
1238
|
+
behavior_spec_id: z.string(),
|
|
1239
|
+
run_number: z.number().int(),
|
|
1240
|
+
snapshot_sha256: z.string(),
|
|
1241
|
+
system_prompt_sha256: z.string(),
|
|
1242
|
+
file_path: z.string().optional(),
|
|
1243
|
+
file_sha256: z.string().optional()
|
|
1244
|
+
}).strict(),
|
|
1245
|
+
base_model: z.object({
|
|
1246
|
+
id: z.string(),
|
|
1247
|
+
revision: z.string().nullable(),
|
|
1248
|
+
fingerprint: z.string().nullable()
|
|
1249
|
+
}).strict(),
|
|
1250
|
+
data: z.object({
|
|
1251
|
+
prebuilt: z.boolean(),
|
|
1252
|
+
format: z.string().nullable(),
|
|
1253
|
+
file_sha256: z.record(z.string(), z.string()),
|
|
1254
|
+
compiled_training_sha256: z.string().nullable(),
|
|
1255
|
+
training_examples: z.number().int().nonnegative().nullable(),
|
|
1256
|
+
eval_split: z.string(),
|
|
1257
|
+
eval_sample_seed: z.number().int(),
|
|
1258
|
+
eval_examples_total: z.number().int().nonnegative(),
|
|
1259
|
+
eval_examples_used: z.number().int().nonnegative()
|
|
1260
|
+
}).strict(),
|
|
1261
|
+
training: z.object({
|
|
1262
|
+
hyperparameters: z.record(z.string(), z.unknown()),
|
|
1263
|
+
exit_code: z.number().int().nullable(),
|
|
1264
|
+
log_uri: z.string()
|
|
1265
|
+
}).strict(),
|
|
1266
|
+
/** Hash over request, runtime, preparation config and input digests. */
|
|
1267
|
+
source_fingerprint: z.string()
|
|
1268
|
+
}).strict();
|
|
1146
1269
|
var runReportSchema = z.object({
|
|
1147
1270
|
run_id: z.string().uuid(),
|
|
1148
1271
|
behavior_spec_id: z.string().uuid(),
|
|
@@ -1168,6 +1291,7 @@ var runReportSchema = z.object({
|
|
|
1168
1291
|
failures: z.array(z.string())
|
|
1169
1292
|
}).strict().optional(),
|
|
1170
1293
|
training: trainingReportSchema,
|
|
1294
|
+
provenance: runProvenanceSchema.optional(),
|
|
1171
1295
|
artifact_uris: z.object({
|
|
1172
1296
|
dataset: z.string(),
|
|
1173
1297
|
baseline_eval: z.string(),
|
|
@@ -1182,6 +1306,7 @@ var runReportSchema = z.object({
|
|
|
1182
1306
|
dataset_prebuilt: z.boolean(),
|
|
1183
1307
|
dataset_format: datasetFormatSchema.nullable().optional(),
|
|
1184
1308
|
dataset_uri: z.string(),
|
|
1309
|
+
dataset_source: datasetSourceSchema.optional(),
|
|
1185
1310
|
spec_example_count: z.number().int().nonnegative(),
|
|
1186
1311
|
training_example_count: z.number().int().nonnegative().nullable(),
|
|
1187
1312
|
eval_examples_total: z.number().int().nonnegative(),
|
|
@@ -1389,6 +1514,9 @@ async function loadNormalizedChatJsonl(path) {
|
|
|
1389
1514
|
async function normalizeChatJsonlForRelocation(path) {
|
|
1390
1515
|
return (await loadNormalizedChatJsonl(path)).jsonl;
|
|
1391
1516
|
}
|
|
1517
|
+
function datasetInputIdentity(value) {
|
|
1518
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
|
|
1519
|
+
}
|
|
1392
1520
|
function buildSystemMessage(spec) {
|
|
1393
1521
|
const parts = [];
|
|
1394
1522
|
const guidelines = spec.guidelines.map((value) => value.trim()).filter(Boolean);
|
|
@@ -1606,7 +1734,7 @@ function resolveLocalRunInputPaths(raw, inputPath) {
|
|
|
1606
1734
|
const dataset = value.dataset_prebuilt;
|
|
1607
1735
|
if (dataset && typeof dataset === "object" && !Array.isArray(dataset)) {
|
|
1608
1736
|
const fields = dataset;
|
|
1609
|
-
for (const key of ["training", "validation", "test"]) {
|
|
1737
|
+
if (fields.huggingface === void 0) for (const key of ["training", "validation", "test"]) {
|
|
1610
1738
|
if (fields[key] !== void 0) fields[key] = resolveLocalReference(fields[key], baseDirectory);
|
|
1611
1739
|
}
|
|
1612
1740
|
}
|
|
@@ -2186,10 +2314,301 @@ function localRuntimePackageRoot(moduleUrl) {
|
|
|
2186
2314
|
return root;
|
|
2187
2315
|
}
|
|
2188
2316
|
|
|
2317
|
+
// src/local-runtime/audit.ts
|
|
2318
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
2319
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
2320
|
+
import { mkdir as mkdir3, open as open2, readFile as readFile4, rename as rename2, rm, stat as stat3, writeFile as writeFile3 } from "fs/promises";
|
|
2321
|
+
import { hostname } from "os";
|
|
2322
|
+
import { join as join3 } from "path";
|
|
2323
|
+
function canonicalJson3(value) {
|
|
2324
|
+
const normalized = JSON.parse(JSON.stringify(value ?? null));
|
|
2325
|
+
const encode = (entry) => {
|
|
2326
|
+
if (entry === null || typeof entry !== "object") return JSON.stringify(entry);
|
|
2327
|
+
if (Array.isArray(entry)) return `[${entry.map(encode).join(",")}]`;
|
|
2328
|
+
const object = entry;
|
|
2329
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${encode(object[key])}`).join(",")}}`;
|
|
2330
|
+
};
|
|
2331
|
+
return encode(normalized);
|
|
2332
|
+
}
|
|
2333
|
+
function eventHash(event) {
|
|
2334
|
+
const { hash: _hash, ...body } = event;
|
|
2335
|
+
return createHash3("sha256").update(canonicalJson3(body)).digest("hex");
|
|
2336
|
+
}
|
|
2337
|
+
function chainEvent(event, previous) {
|
|
2338
|
+
const body = JSON.parse(JSON.stringify({
|
|
2339
|
+
...event,
|
|
2340
|
+
seq: typeof previous?.seq === "number" ? previous.seq + 1 : 0,
|
|
2341
|
+
prev_hash: previous?.hash ?? null
|
|
2342
|
+
}));
|
|
2343
|
+
return { ...body, hash: eventHash(body) };
|
|
2344
|
+
}
|
|
2345
|
+
function verifyEventChain(events) {
|
|
2346
|
+
let legacy = 0;
|
|
2347
|
+
let unchainedAfter = 0;
|
|
2348
|
+
let previous;
|
|
2349
|
+
const result = (status, brk) => ({
|
|
2350
|
+
status,
|
|
2351
|
+
events: events.length,
|
|
2352
|
+
chained_events: events.filter((event) => event.hash !== void 0).length,
|
|
2353
|
+
legacy_events: legacy,
|
|
2354
|
+
unchained_after_chain: unchainedAfter,
|
|
2355
|
+
head_hash: previous?.hash ?? null,
|
|
2356
|
+
...brk ? { break: brk } : {}
|
|
2357
|
+
});
|
|
2358
|
+
for (const [index, event] of events.entries()) {
|
|
2359
|
+
const chained = event;
|
|
2360
|
+
if (chained.hash === void 0) {
|
|
2361
|
+
if (previous) unchainedAfter += 1;
|
|
2362
|
+
else legacy += 1;
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
const expectedSeq = previous ? (previous.seq ?? -1) + 1 : 0;
|
|
2366
|
+
if (chained.seq !== expectedSeq) {
|
|
2367
|
+
return result("broken", { index, seq: chained.seq ?? null, reason: `Expected sequence ${expectedSeq}; an event was removed, reordered or inserted.` });
|
|
2368
|
+
}
|
|
2369
|
+
if ((chained.prev_hash ?? null) !== (previous?.hash ?? null)) {
|
|
2370
|
+
return result("broken", { index, seq: chained.seq, reason: "The previous-event hash does not match; an earlier event was changed or removed." });
|
|
2371
|
+
}
|
|
2372
|
+
if (eventHash(chained) !== chained.hash) {
|
|
2373
|
+
return result("broken", { index, seq: chained.seq, reason: "The event's contents do not match its hash; it was edited." });
|
|
2374
|
+
}
|
|
2375
|
+
previous = chained;
|
|
2376
|
+
}
|
|
2377
|
+
if (events.length === 0) return result("empty");
|
|
2378
|
+
if (!previous) return result("legacy");
|
|
2379
|
+
if (unchainedAfter > 0) return result("mixed");
|
|
2380
|
+
return result(legacy > 0 ? "legacy" : "verified");
|
|
2381
|
+
}
|
|
2382
|
+
async function sha256File2(path) {
|
|
2383
|
+
const hash = createHash3("sha256");
|
|
2384
|
+
for await (const chunk of createReadStream2(path)) hash.update(chunk);
|
|
2385
|
+
return hash.digest("hex");
|
|
2386
|
+
}
|
|
2387
|
+
function processAlive(pid) {
|
|
2388
|
+
try {
|
|
2389
|
+
process.kill(pid, 0);
|
|
2390
|
+
return true;
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
return error.code === "EPERM";
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async function readLockOwner(lockPath) {
|
|
2396
|
+
try {
|
|
2397
|
+
return JSON.parse(await readFile4(join3(lockPath, "owner.json"), "utf8"));
|
|
2398
|
+
} catch {
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
async function abandonedLockOwner(lockPath, orphanMs, remoteMs) {
|
|
2403
|
+
const metadata = await stat3(lockPath).catch(() => null);
|
|
2404
|
+
if (!metadata) return null;
|
|
2405
|
+
const owner = await readLockOwner(lockPath);
|
|
2406
|
+
const age = Date.now() - metadata.mtimeMs;
|
|
2407
|
+
if (!owner) return age > orphanMs ? "ownerless" : null;
|
|
2408
|
+
if (owner.host === hostname()) return processAlive(owner.pid) ? null : owner;
|
|
2409
|
+
return age > remoteMs ? owner : null;
|
|
2410
|
+
}
|
|
2411
|
+
async function breakAbandonedLock(lockPath, abandoned) {
|
|
2412
|
+
const moved = `${lockPath}.abandoned-${randomUUID3()}`;
|
|
2413
|
+
try {
|
|
2414
|
+
await rename2(lockPath, moved);
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
if (error.code === "ENOENT") return;
|
|
2417
|
+
throw error;
|
|
2418
|
+
}
|
|
2419
|
+
const owner = await readLockOwner(moved);
|
|
2420
|
+
const expected = abandoned === "ownerless" ? void 0 : abandoned.token;
|
|
2421
|
+
if (owner?.token !== expected) {
|
|
2422
|
+
await rename2(moved, lockPath).catch(() => void 0);
|
|
2423
|
+
}
|
|
2424
|
+
await rm(moved, { recursive: true, force: true });
|
|
2425
|
+
}
|
|
2426
|
+
async function withAppendLock(lockPath, action, options = {}) {
|
|
2427
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2428
|
+
const token = randomUUID3();
|
|
2429
|
+
const deadline = Date.now() + timeoutMs;
|
|
2430
|
+
for (; ; ) {
|
|
2431
|
+
try {
|
|
2432
|
+
await mkdir3(lockPath, { mode: 448 });
|
|
2433
|
+
await writeFile3(join3(lockPath, "owner.json"), JSON.stringify({ token, pid: process.pid, host: hostname() }), { mode: 384 });
|
|
2434
|
+
break;
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
if (error.code !== "EEXIST") throw error;
|
|
2437
|
+
const abandoned = await abandonedLockOwner(lockPath, options.orphanMs ?? 5e3, options.remoteMs ?? 6e5);
|
|
2438
|
+
if (abandoned) {
|
|
2439
|
+
await breakAbandonedLock(lockPath, abandoned);
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (Date.now() > deadline) throw new Error(`Timed out waiting for the run event log lock: ${lockPath}`);
|
|
2443
|
+
await new Promise((resolve17) => setTimeout(resolve17, 20));
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
try {
|
|
2447
|
+
return await action();
|
|
2448
|
+
} finally {
|
|
2449
|
+
if ((await readLockOwner(lockPath))?.token === token) await rm(lockPath, { recursive: true, force: true });
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
async function readJsonlTail(path, accept = () => true) {
|
|
2453
|
+
let handle;
|
|
2454
|
+
try {
|
|
2455
|
+
handle = await open2(path, "r");
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
if (error.code === "ENOENT") return { last: void 0, partial: false };
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
const size = (await handle.stat()).size;
|
|
2462
|
+
if (size === 0) return { last: void 0, partial: false };
|
|
2463
|
+
let window = Math.min(size, 64 * 1024);
|
|
2464
|
+
for (; ; ) {
|
|
2465
|
+
const buffer = Buffer.alloc(window);
|
|
2466
|
+
await handle.read(buffer, 0, window, size - window);
|
|
2467
|
+
const text = buffer.toString("utf8");
|
|
2468
|
+
const partial = !text.endsWith("\n");
|
|
2469
|
+
const lines = text.split("\n");
|
|
2470
|
+
const complete = window === size ? lines : lines.slice(1);
|
|
2471
|
+
for (const line of complete.reverse()) {
|
|
2472
|
+
if (!line.trim()) continue;
|
|
2473
|
+
let row;
|
|
2474
|
+
try {
|
|
2475
|
+
row = JSON.parse(line);
|
|
2476
|
+
} catch {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
|
|
2480
|
+
if (accept(row)) return { last: row, partial };
|
|
2481
|
+
}
|
|
2482
|
+
if (window === size) return { last: void 0, partial };
|
|
2483
|
+
window = Math.min(size, window * 4);
|
|
2484
|
+
}
|
|
2485
|
+
} finally {
|
|
2486
|
+
await handle.close();
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function parseJsonlTolerant(text) {
|
|
2490
|
+
const rows = [];
|
|
2491
|
+
let unreadable = 0;
|
|
2492
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2493
|
+
if (!line.trim()) continue;
|
|
2494
|
+
try {
|
|
2495
|
+
const row = JSON.parse(line);
|
|
2496
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) {
|
|
2497
|
+
unreadable += 1;
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
rows.push(row);
|
|
2501
|
+
} catch {
|
|
2502
|
+
unreadable += 1;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return { rows, unreadable };
|
|
2506
|
+
}
|
|
2507
|
+
function buildTimeline(events) {
|
|
2508
|
+
const timeline = [];
|
|
2509
|
+
for (const event of events) {
|
|
2510
|
+
const last2 = timeline.at(-1);
|
|
2511
|
+
if (last2 && last2.stage === event.stage) {
|
|
2512
|
+
last2.status = event.status;
|
|
2513
|
+
last2.message = event.message;
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
if (last2) {
|
|
2517
|
+
last2.ended_at = event.occurred_at;
|
|
2518
|
+
last2.duration_seconds = Math.max(0, (Date.parse(event.occurred_at) - Date.parse(last2.started_at)) / 1e3);
|
|
2519
|
+
}
|
|
2520
|
+
timeline.push({
|
|
2521
|
+
stage: event.stage,
|
|
2522
|
+
status: event.status,
|
|
2523
|
+
message: event.message,
|
|
2524
|
+
started_at: event.occurred_at,
|
|
2525
|
+
ended_at: null,
|
|
2526
|
+
duration_seconds: null
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
const last = timeline.at(-1);
|
|
2530
|
+
if (last && ["completed", "failed", "cancelled", "stage_completed"].includes(last.stage)) {
|
|
2531
|
+
last.ended_at = last.started_at;
|
|
2532
|
+
last.duration_seconds = 0;
|
|
2533
|
+
}
|
|
2534
|
+
return timeline;
|
|
2535
|
+
}
|
|
2536
|
+
async function auditRun(store, runId) {
|
|
2537
|
+
const state = await store.getRun(runId);
|
|
2538
|
+
const log = await store.getRunEventLog(state.id);
|
|
2539
|
+
const events = log.events;
|
|
2540
|
+
const chain = verifyEventChain(events);
|
|
2541
|
+
const completion = [...events].reverse().find((event) => event.stage === "completed");
|
|
2542
|
+
const recorded = typeof completion?.details?.report_sha256 === "string" ? completion.details.report_sha256 : null;
|
|
2543
|
+
const recordedManifest = typeof completion?.details?.artifact_manifest_sha256 === "string" ? completion.details.artifact_manifest_sha256 : null;
|
|
2544
|
+
const files = await Promise.all((await store.getRunReportPaths(state.id)).map(async (path) => {
|
|
2545
|
+
const sha256 = await sha256File2(path);
|
|
2546
|
+
return { path, sha256, matches_recorded: recorded ? sha256 === recorded : null };
|
|
2547
|
+
}));
|
|
2548
|
+
const manifestPath = state.artifact_dir ? join3(state.artifact_dir, "artifact-manifest.json") : null;
|
|
2549
|
+
const manifestFile = manifestPath && (await stat3(manifestPath).catch(() => null))?.isFile() ? await sha256File2(manifestPath).then((sha256) => ({
|
|
2550
|
+
path: manifestPath,
|
|
2551
|
+
sha256,
|
|
2552
|
+
matches_recorded: recordedManifest ? sha256 === recordedManifest : null
|
|
2553
|
+
})) : null;
|
|
2554
|
+
const report = await store.getRunReport(state.id).catch(() => null);
|
|
2555
|
+
const findings = [];
|
|
2556
|
+
let verdict = "verified";
|
|
2557
|
+
const tampered = (finding) => {
|
|
2558
|
+
findings.push(finding);
|
|
2559
|
+
verdict = "tampered";
|
|
2560
|
+
};
|
|
2561
|
+
const incomplete = (finding) => {
|
|
2562
|
+
findings.push(finding);
|
|
2563
|
+
if (verdict === "verified") verdict = "incomplete";
|
|
2564
|
+
};
|
|
2565
|
+
const completed = state.status === "completed";
|
|
2566
|
+
if (chain.status === "broken") {
|
|
2567
|
+
tampered(`Event log chain is broken at event ${chain.break.index}: ${chain.break.reason}`);
|
|
2568
|
+
} else if (chain.status === "empty") {
|
|
2569
|
+
if (completed) tampered("The run is completed but its event log is missing or empty.");
|
|
2570
|
+
else incomplete("The run has no events.");
|
|
2571
|
+
} else if (chain.status === "legacy") {
|
|
2572
|
+
incomplete(`${chain.legacy_events} event(s) were recorded before event chaining (older TT version) and cannot be verified.`);
|
|
2573
|
+
} else if (chain.status === "mixed") {
|
|
2574
|
+
incomplete(`${chain.unchained_after_chain} event(s) were written without hashes after chaining began, e.g. by an older TT process.`);
|
|
2575
|
+
}
|
|
2576
|
+
if (log.unreadable_lines > 0) incomplete(`${log.unreadable_lines} event line(s) are unreadable, likely from an interrupted write.`);
|
|
2577
|
+
for (const file of files) {
|
|
2578
|
+
if (file.matches_recorded === false) {
|
|
2579
|
+
tampered(`Report ${file.path} changed after the run completed (sha256 ${file.sha256.slice(0, 12)}, recorded ${recorded.slice(0, 12)}).`);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (manifestFile?.matches_recorded === false) {
|
|
2583
|
+
tampered(`Artifact manifest ${manifestFile.path} changed after the run completed.`);
|
|
2584
|
+
}
|
|
2585
|
+
if (!completed) {
|
|
2586
|
+
incomplete(`The run is ${state.status}; only completed runs have recorded report digests.`);
|
|
2587
|
+
} else if (!recorded) {
|
|
2588
|
+
if (chain.status === "verified") tampered("The completion event is missing from a chained event log; the log was truncated.");
|
|
2589
|
+
else if (chain.status !== "broken" && chain.status !== "empty") incomplete("No report digest was recorded at completion (older TT version).");
|
|
2590
|
+
} else if (files.length === 0) {
|
|
2591
|
+
incomplete("The recorded report file is missing.");
|
|
2592
|
+
}
|
|
2593
|
+
if (completed && recordedManifest && !manifestFile) incomplete("The artifact manifest recorded at completion is missing.");
|
|
2594
|
+
if (completed && report && !report.provenance) incomplete("The report has no provenance block (older TT version).");
|
|
2595
|
+
return {
|
|
2596
|
+
run_id: state.id,
|
|
2597
|
+
status: state.status,
|
|
2598
|
+
verdict,
|
|
2599
|
+
findings,
|
|
2600
|
+
chain: { ...chain, unreadable_lines: log.unreadable_lines },
|
|
2601
|
+
report: { recorded_sha256: recorded, files },
|
|
2602
|
+
artifact_manifest: { recorded_sha256: recordedManifest, file: manifestFile },
|
|
2603
|
+
timeline: buildTimeline(events),
|
|
2604
|
+
provenance: report?.provenance ?? null
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2189
2608
|
// src/local-runtime/store.ts
|
|
2190
|
-
import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as
|
|
2191
|
-
import { dirname as dirname4, join as
|
|
2192
|
-
import { randomUUID as
|
|
2609
|
+
import { appendFile, chmod, copyFile as copyFile2, lstat as lstat3, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, rm as rm2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
|
|
2610
|
+
import { dirname as dirname4, join as join4, resolve as resolve6, sep as sep3 } from "path";
|
|
2611
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2193
2612
|
function isTerminalRunState(state) {
|
|
2194
2613
|
return state.status === "stage_completed" || state.status === "completed" || state.status === "failed" || state.status === "cancelled" && state.current_stage === "cancelled" && Boolean(state.completed_at);
|
|
2195
2614
|
}
|
|
@@ -2199,22 +2618,22 @@ function defaultLocalHome() {
|
|
|
2199
2618
|
function localStorePaths(root) {
|
|
2200
2619
|
return {
|
|
2201
2620
|
root,
|
|
2202
|
-
specsDir:
|
|
2203
|
-
runsDir:
|
|
2204
|
-
modelsDir:
|
|
2205
|
-
datasetsDir:
|
|
2621
|
+
specsDir: join4(root, "specs"),
|
|
2622
|
+
runsDir: join4(root, "runs"),
|
|
2623
|
+
modelsDir: join4(root, "models"),
|
|
2624
|
+
datasetsDir: join4(root, "datasets")
|
|
2206
2625
|
};
|
|
2207
2626
|
}
|
|
2208
2627
|
async function exists2(path) {
|
|
2209
2628
|
try {
|
|
2210
|
-
await
|
|
2629
|
+
await stat4(path);
|
|
2211
2630
|
return true;
|
|
2212
2631
|
} catch {
|
|
2213
2632
|
return false;
|
|
2214
2633
|
}
|
|
2215
2634
|
}
|
|
2216
2635
|
async function ensurePrivateDirectory(path) {
|
|
2217
|
-
await
|
|
2636
|
+
await mkdir4(path, { recursive: true, mode: 448 });
|
|
2218
2637
|
const metadata = await lstat3(path);
|
|
2219
2638
|
if (metadata.isSymbolicLink()) {
|
|
2220
2639
|
throw new Error(`Refusing symbolic link in local store: ${path}`);
|
|
@@ -2232,7 +2651,7 @@ async function hardenPrivateTree(path) {
|
|
|
2232
2651
|
if (metadata.isDirectory()) {
|
|
2233
2652
|
await chmod(path, 448);
|
|
2234
2653
|
for (const entry of await readdir3(path)) {
|
|
2235
|
-
await hardenPrivateTree(
|
|
2654
|
+
await hardenPrivateTree(join4(path, entry));
|
|
2236
2655
|
}
|
|
2237
2656
|
return;
|
|
2238
2657
|
}
|
|
@@ -2262,17 +2681,17 @@ async function hardenExistingLocalStore(root) {
|
|
|
2262
2681
|
}
|
|
2263
2682
|
async function writePrivateJsonAtomic(path, value) {
|
|
2264
2683
|
await ensurePrivateDirectory(dirname4(path));
|
|
2265
|
-
const tmp = `${path}.${process.pid}.${Date.now()}.${
|
|
2266
|
-
await
|
|
2684
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID4()}.tmp`;
|
|
2685
|
+
await writeFile4(tmp, `${JSON.stringify(value, null, 2)}
|
|
2267
2686
|
`, {
|
|
2268
2687
|
encoding: "utf8",
|
|
2269
2688
|
mode: 384
|
|
2270
2689
|
});
|
|
2271
|
-
await
|
|
2690
|
+
await rename3(tmp, path);
|
|
2272
2691
|
await chmod(path, 384);
|
|
2273
2692
|
}
|
|
2274
2693
|
async function readJson2(path) {
|
|
2275
|
-
return JSON.parse(await
|
|
2694
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
2276
2695
|
}
|
|
2277
2696
|
async function appendJsonl(path, value) {
|
|
2278
2697
|
await ensurePrivateDirectory(dirname4(path));
|
|
@@ -2285,13 +2704,9 @@ async function appendJsonl(path, value) {
|
|
|
2285
2704
|
}
|
|
2286
2705
|
async function writePrivateText(path, value) {
|
|
2287
2706
|
await ensurePrivateDirectory(dirname4(path));
|
|
2288
|
-
await
|
|
2707
|
+
await writeFile4(path, value, { encoding: "utf8", mode: 384 });
|
|
2289
2708
|
await chmod(path, 384);
|
|
2290
2709
|
}
|
|
2291
|
-
async function readJsonl(path) {
|
|
2292
|
-
if (!await exists2(path)) return [];
|
|
2293
|
-
return (await readFile4(path, "utf8")).split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
2294
|
-
}
|
|
2295
2710
|
async function copyIfExists(from, to) {
|
|
2296
2711
|
if (!await exists2(from)) return;
|
|
2297
2712
|
await ensurePrivateDirectory(dirname4(to));
|
|
@@ -2330,15 +2745,15 @@ function isPathInside(root, candidate) {
|
|
|
2330
2745
|
function createLocalStore(root = defaultLocalHome()) {
|
|
2331
2746
|
const resolvedRoot = resolve6(root);
|
|
2332
2747
|
const paths = localStorePaths(resolvedRoot);
|
|
2333
|
-
const runDir = (id) =>
|
|
2334
|
-
const runStatePath = (id) =>
|
|
2335
|
-
const runEventsPath = (id) =>
|
|
2336
|
-
const runRequestPath = (id) =>
|
|
2337
|
-
const runReportPath = (id) =>
|
|
2338
|
-
const cancellationPath = (id) =>
|
|
2339
|
-
const specDir = (id) =>
|
|
2340
|
-
const specPath = (id) =>
|
|
2341
|
-
const modelPath = (id) =>
|
|
2748
|
+
const runDir = (id) => join4(paths.runsDir, id);
|
|
2749
|
+
const runStatePath = (id) => join4(runDir(id), "state.json");
|
|
2750
|
+
const runEventsPath = (id) => join4(runDir(id), "progress.jsonl");
|
|
2751
|
+
const runRequestPath = (id) => join4(runDir(id), "request.json");
|
|
2752
|
+
const runReportPath = (id) => join4(runDir(id), "run-report.json");
|
|
2753
|
+
const cancellationPath = (id) => join4(runDir(id), "cancel.requested");
|
|
2754
|
+
const specDir = (id) => join4(paths.specsDir, id);
|
|
2755
|
+
const specPath = (id) => join4(specDir(id), "spec.json");
|
|
2756
|
+
const modelPath = (id) => join4(paths.modelsDir, id, "model.json");
|
|
2342
2757
|
let ensurePromise;
|
|
2343
2758
|
async function ensureOnce() {
|
|
2344
2759
|
await ensurePrivateDirectory(paths.root);
|
|
@@ -2366,8 +2781,8 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2366
2781
|
if (!await exists2(path)) return null;
|
|
2367
2782
|
const [spec, fileStats, directoryStats] = await Promise.all([
|
|
2368
2783
|
readJson2(path),
|
|
2369
|
-
|
|
2370
|
-
|
|
2784
|
+
stat4(path),
|
|
2785
|
+
stat4(specDir(id))
|
|
2371
2786
|
]);
|
|
2372
2787
|
const createdAt = directoryStats.birthtimeMs > 0 ? directoryStats.birthtime : directoryStats.ctime;
|
|
2373
2788
|
return {
|
|
@@ -2414,15 +2829,30 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2414
2829
|
if (!record) throw new Error(`Failed to persist spec: ${specId}`);
|
|
2415
2830
|
return record;
|
|
2416
2831
|
}
|
|
2832
|
+
async function readRunEventLog(id) {
|
|
2833
|
+
const state = await getRun(id);
|
|
2834
|
+
const path = runEventsPath(state.id);
|
|
2835
|
+
if (!await exists2(path)) return { events: [], unreadable_lines: 0 };
|
|
2836
|
+
const parsed = parseJsonlTolerant(await readFile5(path, "utf8"));
|
|
2837
|
+
return { events: parsed.rows, unreadable_lines: parsed.unreadable };
|
|
2838
|
+
}
|
|
2417
2839
|
async function appendRunEvent(state, event) {
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2840
|
+
const path = runEventsPath(state.id);
|
|
2841
|
+
await ensurePrivateDirectory(dirname4(path));
|
|
2842
|
+
const row = await withAppendLock(`${path}.lock`, async () => {
|
|
2843
|
+
const { partial } = await readJsonlTail(path);
|
|
2844
|
+
const { last: previous } = await readJsonlTail(path, (row2) => typeof row2.hash === "string");
|
|
2845
|
+
if (partial) await appendFile(path, "\n", { encoding: "utf8", mode: 384 });
|
|
2846
|
+
const chained = chainEvent({
|
|
2847
|
+
id: randomUUID4(),
|
|
2848
|
+
run_id: state.id,
|
|
2849
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2850
|
+
...event
|
|
2851
|
+
}, previous);
|
|
2852
|
+
await appendJsonl(path, chained);
|
|
2853
|
+
return chained;
|
|
2854
|
+
});
|
|
2855
|
+
await appendJsonl(join4(state.artifact_dir, "progress.jsonl"), {
|
|
2426
2856
|
at: row.occurred_at,
|
|
2427
2857
|
stage: row.stage,
|
|
2428
2858
|
status: row.status,
|
|
@@ -2453,8 +2883,8 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2453
2883
|
const previous = await getRun(runId);
|
|
2454
2884
|
if (isTerminalRunState(previous)) return previous;
|
|
2455
2885
|
await Promise.all([
|
|
2456
|
-
|
|
2457
|
-
|
|
2886
|
+
rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true }),
|
|
2887
|
+
rm2(runReportPath(previous.id), { force: true })
|
|
2458
2888
|
]);
|
|
2459
2889
|
if (previous.status === "cancelled" && previous.current_stage === "cancel_requested") return previous;
|
|
2460
2890
|
const requested = {
|
|
@@ -2620,7 +3050,7 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2620
3050
|
createdAt: completedAt
|
|
2621
3051
|
});
|
|
2622
3052
|
if (!model) {
|
|
2623
|
-
await
|
|
3053
|
+
await rm2(join4(paths.modelsDir, `local-${report.run_id}`), { recursive: true, force: true });
|
|
2624
3054
|
}
|
|
2625
3055
|
const previous = await getRun(report.run_id);
|
|
2626
3056
|
const state = {
|
|
@@ -2641,20 +3071,27 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2641
3071
|
if (await exists2(cancellationPath(report.run_id))) {
|
|
2642
3072
|
return preserveCancellationRequestState(report.run_id);
|
|
2643
3073
|
}
|
|
3074
|
+
const manifestPath = join4(artifactDir, "artifact-manifest.json");
|
|
2644
3075
|
await appendRunEvent(state, {
|
|
2645
3076
|
stage: "completed",
|
|
2646
3077
|
status: "completed",
|
|
2647
3078
|
message: "Run completed successfully.",
|
|
2648
|
-
details: {
|
|
3079
|
+
details: {
|
|
3080
|
+
report_path: reportPath,
|
|
3081
|
+
// Pin the published evidence: `tt runs audit` re-hashes these files.
|
|
3082
|
+
report_sha256: await sha256File2(runReportPath(report.run_id)),
|
|
3083
|
+
...await exists2(manifestPath) ? { artifact_manifest_sha256: await sha256File2(manifestPath) } : {},
|
|
3084
|
+
...model ? { model_id: model.id } : {}
|
|
3085
|
+
}
|
|
2649
3086
|
});
|
|
2650
3087
|
return state;
|
|
2651
3088
|
},
|
|
2652
3089
|
registerModel: registerModelRecord,
|
|
2653
3090
|
async invalidateRunOutputs(runId, options) {
|
|
2654
3091
|
const previous = await getRun(runId);
|
|
2655
|
-
if (options.report) await
|
|
3092
|
+
if (options.report) await rm2(runReportPath(previous.id), { force: true });
|
|
2656
3093
|
if (options.model) {
|
|
2657
|
-
await
|
|
3094
|
+
await rm2(join4(paths.modelsDir, `local-${previous.id}`), { recursive: true, force: true });
|
|
2658
3095
|
}
|
|
2659
3096
|
const state = {
|
|
2660
3097
|
...previous,
|
|
@@ -2697,7 +3134,7 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2697
3134
|
`);
|
|
2698
3135
|
const latest = await getRun(state.id);
|
|
2699
3136
|
if (isTerminalRunState(latest)) {
|
|
2700
|
-
if (latest.status !== "cancelled") await
|
|
3137
|
+
if (latest.status !== "cancelled") await rm2(cancellationPath(state.id), { force: true });
|
|
2701
3138
|
return;
|
|
2702
3139
|
}
|
|
2703
3140
|
const updated = {
|
|
@@ -2724,8 +3161,23 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2724
3161
|
},
|
|
2725
3162
|
getRun,
|
|
2726
3163
|
async getRunEvents(id) {
|
|
3164
|
+
return (await readRunEventLog(id)).events;
|
|
3165
|
+
},
|
|
3166
|
+
getRunEventLog: readRunEventLog,
|
|
3167
|
+
async getRunReportPaths(id) {
|
|
2727
3168
|
const state = await getRun(id);
|
|
2728
|
-
|
|
3169
|
+
const candidates = [];
|
|
3170
|
+
if (state.report_path) {
|
|
3171
|
+
const reportPath = resolve6(state.report_path);
|
|
3172
|
+
const allowedRoots = [resolvedRoot, ...state.artifact_dir ? [resolve6(state.artifact_dir)] : []];
|
|
3173
|
+
if (allowedRoots.some((allowed) => isPathInside(allowed, reportPath))) candidates.push(reportPath);
|
|
3174
|
+
}
|
|
3175
|
+
candidates.push(runReportPath(state.id));
|
|
3176
|
+
const existing = [];
|
|
3177
|
+
for (const path of new Set(candidates)) {
|
|
3178
|
+
if ((await lstat3(path).catch(() => null))?.isFile()) existing.push(path);
|
|
3179
|
+
}
|
|
3180
|
+
return existing;
|
|
2729
3181
|
},
|
|
2730
3182
|
async getRunReport(id) {
|
|
2731
3183
|
const state = await getRun(id);
|
|
@@ -2756,41 +3208,194 @@ function createLocalStore(root = defaultLocalHome()) {
|
|
|
2756
3208
|
}
|
|
2757
3209
|
|
|
2758
3210
|
// src/local-runtime/prefetch.ts
|
|
2759
|
-
import { createHash as
|
|
2760
|
-
import { createReadStream as
|
|
2761
|
-
import { lstat as lstat4, mkdir as
|
|
2762
|
-
import { isAbsolute as isAbsolute4, join as
|
|
3211
|
+
import { createHash as createHash5 } from "crypto";
|
|
3212
|
+
import { createReadStream as createReadStream3 } from "fs";
|
|
3213
|
+
import { lstat as lstat4, mkdir as mkdir6, readFile as readFile6, readdir as readdir4, realpath as realpath2, stat as stat5 } from "fs/promises";
|
|
3214
|
+
import { isAbsolute as isAbsolute4, join as join7, relative as relative3, resolve as resolve9, sep as sep4 } from "path";
|
|
2763
3215
|
|
|
2764
3216
|
// src/local-runtime/process-runner.ts
|
|
2765
|
-
import { createHash as
|
|
3217
|
+
import { createHash as createHash4 } from "crypto";
|
|
2766
3218
|
import { spawn } from "child_process";
|
|
2767
3219
|
import { constants, readFileSync } from "fs";
|
|
2768
|
-
import { mkdir as
|
|
2769
|
-
import { dirname as dirname5, join as
|
|
3220
|
+
import { mkdir as mkdir5, open as open3 } from "fs/promises";
|
|
3221
|
+
import { dirname as dirname5, join as join5, resolve as resolve7 } from "path";
|
|
3222
|
+
|
|
3223
|
+
// src/local-runtime/progress-bar.ts
|
|
3224
|
+
var PROGRESS_PREFIX = "@@tt-progress ";
|
|
3225
|
+
function nonNegativeInteger(value) {
|
|
3226
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
3227
|
+
}
|
|
3228
|
+
function parseProgressLine(line) {
|
|
3229
|
+
const start = line.indexOf(PROGRESS_PREFIX);
|
|
3230
|
+
if (start !== 0) return null;
|
|
3231
|
+
let value;
|
|
3232
|
+
try {
|
|
3233
|
+
value = JSON.parse(line.slice(PROGRESS_PREFIX.length));
|
|
3234
|
+
} catch {
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
3237
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3238
|
+
const record = value;
|
|
3239
|
+
const completed = nonNegativeInteger(record.completed_bytes);
|
|
3240
|
+
const total = nonNegativeInteger(record.total_bytes);
|
|
3241
|
+
if (completed === null || total === null) return null;
|
|
3242
|
+
return {
|
|
3243
|
+
label: typeof record.label === "string" ? record.label : "download",
|
|
3244
|
+
phase: typeof record.phase === "string" ? record.phase : "download",
|
|
3245
|
+
completed_bytes: completed,
|
|
3246
|
+
total_bytes: total,
|
|
3247
|
+
files_completed: nonNegativeInteger(record.files_completed) ?? 0,
|
|
3248
|
+
files_total: nonNegativeInteger(record.files_total) ?? 0
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
function formatBytes(bytes) {
|
|
3252
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3253
|
+
let value = bytes;
|
|
3254
|
+
let unit = 0;
|
|
3255
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
3256
|
+
value /= 1024;
|
|
3257
|
+
unit += 1;
|
|
3258
|
+
}
|
|
3259
|
+
return `${unit === 0 ? value.toFixed(0) : value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unit]}`;
|
|
3260
|
+
}
|
|
3261
|
+
function formatDuration(seconds) {
|
|
3262
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
|
3263
|
+
const whole = Math.round(seconds);
|
|
3264
|
+
const hours = Math.floor(whole / 3600);
|
|
3265
|
+
const minutes = Math.floor(whole % 3600 / 60);
|
|
3266
|
+
const rest = whole % 60;
|
|
3267
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
3268
|
+
if (minutes > 0) return `${minutes}m ${String(rest).padStart(2, "0")}s`;
|
|
3269
|
+
return `${rest}s`;
|
|
3270
|
+
}
|
|
3271
|
+
var RateTracker = class {
|
|
3272
|
+
constructor(windowMs = 15e3) {
|
|
3273
|
+
this.windowMs = windowMs;
|
|
3274
|
+
}
|
|
3275
|
+
samples = [];
|
|
3276
|
+
add(at, bytes) {
|
|
3277
|
+
const last = this.samples.at(-1);
|
|
3278
|
+
if (last && bytes < last.bytes) this.samples = [];
|
|
3279
|
+
this.samples.push({ at, bytes });
|
|
3280
|
+
while (this.samples.length > 2 && at - this.samples[0].at > this.windowMs) this.samples.shift();
|
|
3281
|
+
}
|
|
3282
|
+
bytesPerSecond() {
|
|
3283
|
+
const first = this.samples[0];
|
|
3284
|
+
const last = this.samples.at(-1);
|
|
3285
|
+
if (!first || !last || last.at - first.at < 500) return null;
|
|
3286
|
+
return (last.bytes - first.bytes) / ((last.at - first.at) / 1e3);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
function formatProgressLine(progress, rate, columns) {
|
|
3290
|
+
const total = progress.total_bytes;
|
|
3291
|
+
const fraction = total > 0 ? Math.min(1, progress.completed_bytes / total) : 1;
|
|
3292
|
+
const fields = [
|
|
3293
|
+
{ text: `${(fraction * 100).toFixed(1).padStart(5)}%` },
|
|
3294
|
+
{ text: `${formatBytes(progress.completed_bytes)} / ${formatBytes(total)}` }
|
|
3295
|
+
];
|
|
3296
|
+
const done = progress.completed_bytes >= total;
|
|
3297
|
+
if (!done && rate !== null && rate > 0) {
|
|
3298
|
+
fields.push(
|
|
3299
|
+
{ text: `${formatBytes(rate)}/s`, drop: 2 },
|
|
3300
|
+
{ text: `ETA ${formatDuration((total - progress.completed_bytes) / rate)}`, drop: 3 }
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
if (progress.files_total > 0) fields.push({ text: `${progress.files_completed}/${progress.files_total} files`, drop: 1 });
|
|
3304
|
+
const prefix = `[tt] ${progress.label}: `;
|
|
3305
|
+
const join16 = () => fields.map((field) => field.text).join(" ");
|
|
3306
|
+
if (columns === void 0) return `${prefix}${join16()}`;
|
|
3307
|
+
for (const rank of [1, 2, 3]) {
|
|
3308
|
+
if (prefix.length + join16().length <= columns - 1) break;
|
|
3309
|
+
const index = fields.findIndex((field) => field.drop === rank);
|
|
3310
|
+
if (index !== -1) fields.splice(index, 1);
|
|
3311
|
+
}
|
|
3312
|
+
const text = join16();
|
|
3313
|
+
const barWidth = Math.min(30, columns - 1 - prefix.length - text.length - 3);
|
|
3314
|
+
if (barWidth < 10) return `${prefix}${text}`.slice(0, Math.max(0, columns - 1));
|
|
3315
|
+
const filled = Math.round(fraction * barWidth);
|
|
3316
|
+
return `${prefix}[${"#".repeat(filled)}${"-".repeat(barWidth - filled)}] ${text}`;
|
|
3317
|
+
}
|
|
3318
|
+
function createProgressRenderer(options) {
|
|
3319
|
+
const now = options.now ?? (() => Date.now());
|
|
3320
|
+
const rates = /* @__PURE__ */ new Map();
|
|
3321
|
+
const lastLine = /* @__PURE__ */ new Map();
|
|
3322
|
+
const completed = /* @__PURE__ */ new Set();
|
|
3323
|
+
let drawn = false;
|
|
3324
|
+
let lastDraw = Number.NEGATIVE_INFINITY;
|
|
3325
|
+
const finished = (progress) => progress.phase === "downloaded" || progress.total_bytes > 0 && progress.completed_bytes >= progress.total_bytes;
|
|
3326
|
+
return {
|
|
3327
|
+
update(progress) {
|
|
3328
|
+
const at = now();
|
|
3329
|
+
const tracker = rates.get(progress.label) ?? new RateTracker();
|
|
3330
|
+
rates.set(progress.label, tracker);
|
|
3331
|
+
tracker.add(at, progress.completed_bytes);
|
|
3332
|
+
const done = finished(progress);
|
|
3333
|
+
if (options.tty) {
|
|
3334
|
+
if (done && completed.has(progress.label)) return;
|
|
3335
|
+
if (!done) completed.delete(progress.label);
|
|
3336
|
+
if (!done && at - lastDraw < 100) return;
|
|
3337
|
+
lastDraw = at;
|
|
3338
|
+
const line = formatProgressLine(progress, tracker.bytesPerSecond(), Math.max(40, options.columns ?? 80));
|
|
3339
|
+
options.write(`\r\x1B[2K${line}${done ? "\n" : ""}`);
|
|
3340
|
+
drawn = !done;
|
|
3341
|
+
if (done) {
|
|
3342
|
+
rates.delete(progress.label);
|
|
3343
|
+
completed.add(progress.label);
|
|
3344
|
+
}
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const fraction = progress.total_bytes > 0 ? progress.completed_bytes / progress.total_bytes : 1;
|
|
3348
|
+
const decile = Math.floor(fraction * 10);
|
|
3349
|
+
const previous = lastLine.get(progress.label);
|
|
3350
|
+
const interval = options.lineIntervalMs ?? 3e4;
|
|
3351
|
+
if (previous && !done && decile <= previous.decile && at - previous.at < interval) return;
|
|
3352
|
+
if (previous && done && previous.decile === 10) return;
|
|
3353
|
+
lastLine.set(progress.label, { at, decile: done ? 10 : decile });
|
|
3354
|
+
options.write(`${formatProgressLine(progress, tracker.bytesPerSecond())}
|
|
3355
|
+
`);
|
|
3356
|
+
},
|
|
3357
|
+
interrupt() {
|
|
3358
|
+
if (!drawn) return;
|
|
3359
|
+
options.write("\r\x1B[2K");
|
|
3360
|
+
drawn = false;
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
function progressTty(env = process.env, stream = process.stderr) {
|
|
3365
|
+
if (env.TT_PROGRESS_TTY === "0") return false;
|
|
3366
|
+
return env.TT_PROGRESS_TTY === "1" || Boolean(stream.isTTY);
|
|
3367
|
+
}
|
|
3368
|
+
function progressColumns(env = process.env, stream = process.stderr) {
|
|
3369
|
+
const hinted = Number(env.TT_PROGRESS_COLUMNS);
|
|
3370
|
+
if (Number.isInteger(hinted) && hinted > 0) return hinted;
|
|
3371
|
+
return stream.columns;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/local-runtime/process-runner.ts
|
|
2770
3375
|
var packageRoot = localRuntimePackageRoot(import.meta.url);
|
|
2771
|
-
var bundledProject =
|
|
3376
|
+
var bundledProject = join5(packageRoot, "training/adapter");
|
|
2772
3377
|
var bundledRuntimeHash = (() => {
|
|
2773
|
-
const hash =
|
|
3378
|
+
const hash = createHash4("sha256");
|
|
2774
3379
|
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
2775
|
-
hash.update(readFileSync(
|
|
3380
|
+
hash.update(readFileSync(join5(bundledProject, name)));
|
|
2776
3381
|
}
|
|
2777
3382
|
return hash.digest("hex").slice(0, 20);
|
|
2778
3383
|
})();
|
|
2779
3384
|
var BUNDLED_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv", bundledRuntimeHash);
|
|
2780
|
-
var foundationProject =
|
|
3385
|
+
var foundationProject = join5(packageRoot, "training/foundation");
|
|
2781
3386
|
var foundationRuntimeHash = (() => {
|
|
2782
|
-
const hash =
|
|
3387
|
+
const hash = createHash4("sha256");
|
|
2783
3388
|
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
2784
|
-
hash.update(readFileSync(
|
|
3389
|
+
hash.update(readFileSync(join5(foundationProject, name)));
|
|
2785
3390
|
}
|
|
2786
3391
|
return hash.digest("hex").slice(0, 20);
|
|
2787
3392
|
})();
|
|
2788
3393
|
var FOUNDATION_PYTHON_ENVIRONMENT = pythonEnvironmentPath("uv-foundation", foundationRuntimeHash);
|
|
2789
|
-
var decisionProject =
|
|
3394
|
+
var decisionProject = join5(packageRoot, "training/decision");
|
|
2790
3395
|
var decisionRuntimeHash = (() => {
|
|
2791
|
-
const hash =
|
|
3396
|
+
const hash = createHash4("sha256");
|
|
2792
3397
|
for (const name of ["pyproject.toml", "uv.lock"]) {
|
|
2793
|
-
hash.update(readFileSync(
|
|
3398
|
+
hash.update(readFileSync(join5(decisionProject, name)));
|
|
2794
3399
|
}
|
|
2795
3400
|
return hash.digest("hex").slice(0, 20);
|
|
2796
3401
|
})();
|
|
@@ -2803,7 +3408,7 @@ function buildDecisionPythonCommand(entrypoint, args = []) {
|
|
|
2803
3408
|
"--project",
|
|
2804
3409
|
decisionProject,
|
|
2805
3410
|
"python",
|
|
2806
|
-
|
|
3411
|
+
join5(decisionProject, "src", entrypoint),
|
|
2807
3412
|
...args
|
|
2808
3413
|
];
|
|
2809
3414
|
return { command: "uv", commandArgs, displayCommand: ["uv", ...commandArgs] };
|
|
@@ -2812,7 +3417,7 @@ function withDecisionPythonEnvironment(env) {
|
|
|
2812
3417
|
return { ...env, UV_PROJECT_ENVIRONMENT: DECISION_PYTHON_ENVIRONMENT };
|
|
2813
3418
|
}
|
|
2814
3419
|
function buildBundledPythonCommand(entrypoint, args = []) {
|
|
2815
|
-
const target = entrypoint === "-c" ? entrypoint :
|
|
3420
|
+
const target = entrypoint === "-c" ? entrypoint : join5(bundledProject, "src", entrypoint);
|
|
2816
3421
|
const commandArgs = [
|
|
2817
3422
|
"run",
|
|
2818
3423
|
"--frozen",
|
|
@@ -2836,7 +3441,7 @@ function withBundledPythonEnvironment(env) {
|
|
|
2836
3441
|
};
|
|
2837
3442
|
}
|
|
2838
3443
|
function buildFoundationPythonCommand(entrypoint, args = []) {
|
|
2839
|
-
const target = entrypoint === "-c" ? entrypoint :
|
|
3444
|
+
const target = entrypoint === "-c" ? entrypoint : join5(foundationProject, "src", entrypoint);
|
|
2840
3445
|
const commandArgs = [
|
|
2841
3446
|
"run",
|
|
2842
3447
|
"--frozen",
|
|
@@ -2867,7 +3472,7 @@ var ProcessCancelledError = class extends Error {
|
|
|
2867
3472
|
};
|
|
2868
3473
|
async function openProcessLog(path, exclusive, append) {
|
|
2869
3474
|
const flags = constants.O_WRONLY | constants.O_CREAT | (exclusive ? constants.O_EXCL : append ? constants.O_APPEND : constants.O_TRUNC) | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW);
|
|
2870
|
-
const handle = await
|
|
3475
|
+
const handle = await open3(path, flags, 384);
|
|
2871
3476
|
try {
|
|
2872
3477
|
return handle.createWriteStream({ autoClose: true });
|
|
2873
3478
|
} catch (error) {
|
|
@@ -2876,7 +3481,7 @@ async function openProcessLog(path, exclusive, append) {
|
|
|
2876
3481
|
}
|
|
2877
3482
|
}
|
|
2878
3483
|
async function runLoggedProcess(args) {
|
|
2879
|
-
if (args.logPath) await
|
|
3484
|
+
if (args.logPath) await mkdir5(dirname5(args.logPath), { recursive: true });
|
|
2880
3485
|
const logStream = args.logPath ? await openProcessLog(args.logPath, args.exclusiveLog ?? false, args.appendLog ?? false) : null;
|
|
2881
3486
|
let stderr = "";
|
|
2882
3487
|
try {
|
|
@@ -2950,6 +3555,11 @@ async function runLoggedProcess(args) {
|
|
|
2950
3555
|
child.stderr.pipe(logStream, { end: false });
|
|
2951
3556
|
}
|
|
2952
3557
|
forwardStreamLines(child.stdout, (line) => {
|
|
3558
|
+
const progress = parseProgressLine(line);
|
|
3559
|
+
if (progress) {
|
|
3560
|
+
reportInBackground(() => args.reporter?.onProgress?.(progress));
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
2953
3563
|
args.onLine?.(line, "stdout");
|
|
2954
3564
|
if (args.reporter?.verbose) {
|
|
2955
3565
|
reportInBackground(() => args.reporter?.onLog?.({ stage: args.stage, stream: "stdout", message: line }));
|
|
@@ -2999,7 +3609,7 @@ async function runLoggedProcess(args) {
|
|
|
2999
3609
|
|
|
3000
3610
|
// src/local-runtime/huggingface-cache.ts
|
|
3001
3611
|
import { homedir as homedir3 } from "os";
|
|
3002
|
-
import { dirname as dirname6, join as
|
|
3612
|
+
import { dirname as dirname6, join as join6, resolve as resolve8 } from "path";
|
|
3003
3613
|
function minimalMachineLearningEnvironment(env) {
|
|
3004
3614
|
const exact = /* @__PURE__ */ new Set([
|
|
3005
3615
|
"PATH",
|
|
@@ -3031,6 +3641,9 @@ function minimalMachineLearningEnvironment(env) {
|
|
|
3031
3641
|
"HF_HUB_DISABLE_TELEMETRY",
|
|
3032
3642
|
"TRANSFORMERS_OFFLINE",
|
|
3033
3643
|
"HF_HUB_ENABLE_HF_TRANSFER",
|
|
3644
|
+
"HF_HUB_DOWNLOAD_TIMEOUT",
|
|
3645
|
+
"HF_HUB_ETAG_TIMEOUT",
|
|
3646
|
+
"TT_HF_DOWNLOAD_MAX_ATTEMPTS",
|
|
3034
3647
|
"PYTHONUTF8",
|
|
3035
3648
|
"PYTHONIOENCODING"
|
|
3036
3649
|
]);
|
|
@@ -3056,12 +3669,12 @@ function resolveHuggingFaceCacheLayout(modelCache) {
|
|
|
3056
3669
|
const hfHome = resolve8(modelCache);
|
|
3057
3670
|
return {
|
|
3058
3671
|
hfHome,
|
|
3059
|
-
hubCache:
|
|
3672
|
+
hubCache: join6(hfHome, "hub")
|
|
3060
3673
|
};
|
|
3061
3674
|
}
|
|
3062
3675
|
function withHuggingFaceCacheEnvironment(env, modelCache) {
|
|
3063
3676
|
const inheritedHub = env.HF_HUB_CACHE ?? env.HUGGINGFACE_HUB_CACHE;
|
|
3064
|
-
const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) :
|
|
3677
|
+
const canonicalHome = modelCache ?? env.HF_HOME ?? (inheritedHub ? dirname6(resolve8(inheritedHub)) : join6(homedir3(), ".cache", "huggingface"));
|
|
3065
3678
|
const layout = resolveHuggingFaceCacheLayout(canonicalHome);
|
|
3066
3679
|
const result = {
|
|
3067
3680
|
...env,
|
|
@@ -3141,10 +3754,10 @@ function certifiedFileDigests(modelId) {
|
|
|
3141
3754
|
}
|
|
3142
3755
|
}
|
|
3143
3756
|
async function certifiedFileDigest(path, expected) {
|
|
3144
|
-
const metadata = await
|
|
3145
|
-
const digest =
|
|
3757
|
+
const metadata = await stat5(path);
|
|
3758
|
+
const digest = createHash5(expected.algorithm);
|
|
3146
3759
|
if (expected.algorithm === "sha1") digest.update(`blob ${metadata.size}\0`);
|
|
3147
|
-
for await (const chunk of
|
|
3760
|
+
for await (const chunk of createReadStream3(path)) digest.update(chunk);
|
|
3148
3761
|
return digest.digest("hex");
|
|
3149
3762
|
}
|
|
3150
3763
|
function isStrictPhysicalDescendant(parent, child) {
|
|
@@ -3154,8 +3767,8 @@ function isStrictPhysicalDescendant(parent, child) {
|
|
|
3154
3767
|
async function verifySnapshotCacheLocation(path, modelId, revision, modelCache) {
|
|
3155
3768
|
const repository = `models--${modelId.replaceAll("/", "--")}`;
|
|
3156
3769
|
const layout = resolveHuggingFaceCacheLayout(modelCache);
|
|
3157
|
-
const repositoryRoot =
|
|
3158
|
-
const expectedPath =
|
|
3770
|
+
const repositoryRoot = join7(layout.hubCache, repository);
|
|
3771
|
+
const expectedPath = join7(repositoryRoot, "snapshots", revision);
|
|
3159
3772
|
const [
|
|
3160
3773
|
physicalCacheRoot,
|
|
3161
3774
|
physicalHubRoot,
|
|
@@ -3167,7 +3780,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
|
|
|
3167
3780
|
realpath2(layout.hfHome).catch(() => null),
|
|
3168
3781
|
realpath2(layout.hubCache).catch(() => null),
|
|
3169
3782
|
realpath2(repositoryRoot).catch(() => null),
|
|
3170
|
-
realpath2(
|
|
3783
|
+
realpath2(join7(repositoryRoot, "blobs")).catch(() => null),
|
|
3171
3784
|
realpath2(path).catch(() => null),
|
|
3172
3785
|
realpath2(expectedPath).catch(() => null)
|
|
3173
3786
|
]);
|
|
@@ -3183,7 +3796,7 @@ async function verifySnapshotCacheLocation(path, modelId, revision, modelCache)
|
|
|
3183
3796
|
throw new Error(`Hugging Face repository escapes its configured hub cache: ${repositoryRoot}`);
|
|
3184
3797
|
}
|
|
3185
3798
|
if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalBlobRoot)) {
|
|
3186
|
-
throw new Error(`Hugging Face blob store escapes its repository: ${
|
|
3799
|
+
throw new Error(`Hugging Face blob store escapes its repository: ${join7(repositoryRoot, "blobs")}`);
|
|
3187
3800
|
}
|
|
3188
3801
|
if (!isStrictPhysicalDescendant(physicalRepositoryRoot, physicalExpectedPath)) {
|
|
3189
3802
|
throw new Error(`Hugging Face snapshot escapes its configured repository: ${expectedPath}`);
|
|
@@ -3245,13 +3858,13 @@ async function verifyCertifiedLocalSnapshot(path, modelId, snapshotFiles, modelC
|
|
|
3245
3858
|
const expectedFiles = new Set(Object.keys(digests));
|
|
3246
3859
|
const unexpected = [...actualFiles].filter((name) => !expectedFiles.has(name)).sort();
|
|
3247
3860
|
if (unexpected.length > 0) {
|
|
3248
|
-
throw new Error(`Certified snapshot contains unexpected file: ${
|
|
3861
|
+
throw new Error(`Certified snapshot contains unexpected file: ${join7(path, unexpected[0])}`);
|
|
3249
3862
|
}
|
|
3250
3863
|
for (const [name, expected] of Object.entries(digests)) {
|
|
3251
3864
|
if (!actualFiles.has(name)) {
|
|
3252
|
-
throw new Error(`Certified snapshot file is missing: ${
|
|
3865
|
+
throw new Error(`Certified snapshot file is missing: ${join7(path, name)}`);
|
|
3253
3866
|
}
|
|
3254
|
-
const file =
|
|
3867
|
+
const file = join7(path, name);
|
|
3255
3868
|
const physicalFile = physicalFiles.get(file);
|
|
3256
3869
|
if (await certifiedFileDigest(physicalFile, expected) !== expected.digest) {
|
|
3257
3870
|
throw new Error(`Certified snapshot file checksum mismatch: ${file}`);
|
|
@@ -3294,9 +3907,9 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3294
3907
|
const files = [];
|
|
3295
3908
|
const visit = async (directory) => {
|
|
3296
3909
|
for (const entry of await readdir4(directory, { withFileTypes: true })) {
|
|
3297
|
-
const child =
|
|
3910
|
+
const child = join7(directory, entry.name);
|
|
3298
3911
|
if (entry.isSymbolicLink()) {
|
|
3299
|
-
const metadata2 = await
|
|
3912
|
+
const metadata2 = await stat5(child);
|
|
3300
3913
|
if (!metadata2.isFile()) {
|
|
3301
3914
|
throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
|
|
3302
3915
|
}
|
|
@@ -3307,7 +3920,7 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3307
3920
|
await visit(child);
|
|
3308
3921
|
continue;
|
|
3309
3922
|
}
|
|
3310
|
-
const metadata = await
|
|
3923
|
+
const metadata = await stat5(child);
|
|
3311
3924
|
if (metadata.isFile()) files.push({ path: child, size: metadata.size });
|
|
3312
3925
|
}
|
|
3313
3926
|
};
|
|
@@ -3318,15 +3931,15 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3318
3931
|
throw new Error(`Local base-model directory contains no non-empty Transformers model weights: ${path}`);
|
|
3319
3932
|
}
|
|
3320
3933
|
const required = async (name) => {
|
|
3321
|
-
const metadata = await
|
|
3934
|
+
const metadata = await stat5(join7(path, name)).catch(() => null);
|
|
3322
3935
|
return Boolean(metadata?.isFile() && metadata.size > 0);
|
|
3323
3936
|
};
|
|
3324
3937
|
if (!await required("config.json")) throw new Error(`Local base-model directory is missing config.json: ${path}`);
|
|
3325
3938
|
try {
|
|
3326
|
-
const config = JSON.parse(await
|
|
3939
|
+
const config = JSON.parse(await readFile6(join7(path, "config.json"), "utf8"));
|
|
3327
3940
|
assertCertifiedBaseModelConfig(
|
|
3328
3941
|
config,
|
|
3329
|
-
`Local base-model config ${
|
|
3942
|
+
`Local base-model config ${join7(path, "config.json")}`,
|
|
3330
3943
|
expectedModelId
|
|
3331
3944
|
);
|
|
3332
3945
|
} catch (error) {
|
|
@@ -3355,13 +3968,17 @@ async function verifyLocalBaseModel(path, expectedModelId, modelCache) {
|
|
|
3355
3968
|
if (!await required("tokenizer_config.json") || !(await Promise.all(vocabNames.map(required))).some(Boolean)) {
|
|
3356
3969
|
throw new Error(`Local base-model directory is missing tokenizer metadata or vocabulary: ${path}`);
|
|
3357
3970
|
}
|
|
3971
|
+
const inventory = new Set(nonEmpty.map((file) => resolve9(file.path)));
|
|
3358
3972
|
for (const file of files.filter((candidate) => candidate.path.endsWith(".index.json"))) {
|
|
3359
|
-
const parsed = JSON.parse(await
|
|
3973
|
+
const parsed = JSON.parse(await readFile6(file.path, "utf8"));
|
|
3360
3974
|
if (!parsed.weight_map || typeof parsed.weight_map !== "object" || Array.isArray(parsed.weight_map)) {
|
|
3361
3975
|
throw new Error(`Local base-model weight index is invalid: ${file.path}`);
|
|
3362
3976
|
}
|
|
3363
3977
|
for (const shard of new Set(Object.values(parsed.weight_map))) {
|
|
3364
|
-
if (typeof shard !== "string" || !
|
|
3978
|
+
if (typeof shard !== "string" || !shard || /^(?:[A-Za-z]:|[/\\])/.test(shard) || shard.split(/[/\\]/).includes("..")) {
|
|
3979
|
+
throw new Error("Local base-model weight index has an invalid indexed weight shard path");
|
|
3980
|
+
}
|
|
3981
|
+
if (!inventory.has(resolve9(path, shard)) || !await required(shard)) {
|
|
3365
3982
|
throw new Error(`Local base-model directory is missing indexed weight shard: ${String(shard)}`);
|
|
3366
3983
|
}
|
|
3367
3984
|
}
|
|
@@ -3381,7 +3998,7 @@ async function prefetchBaseModel(args) {
|
|
|
3381
3998
|
args.localOnly ? "verify-base" : "prefetch",
|
|
3382
3999
|
`${safeName(args.request.spec_snapshot.base_model)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
|
|
3383
4000
|
);
|
|
3384
|
-
await
|
|
4001
|
+
await mkdir6(artifactDir, { recursive: true });
|
|
3385
4002
|
if (args.config.paths.baseModel) {
|
|
3386
4003
|
const localPath = resolve9(args.config.paths.baseModel);
|
|
3387
4004
|
const verified = await verifyLocalBaseModel(
|
|
@@ -3400,9 +4017,9 @@ async function prefetchBaseModel(args) {
|
|
|
3400
4017
|
reason: args.localOnly ? "Verified the configured local base-model artifact; no network access was used." : "paths.baseModel points at a verified local base-model artifact; no Hugging Face download is needed."
|
|
3401
4018
|
};
|
|
3402
4019
|
}
|
|
3403
|
-
const inputPath =
|
|
3404
|
-
const outputPath =
|
|
3405
|
-
const logPath =
|
|
4020
|
+
const inputPath = join7(artifactDir, "prefetch-input.json");
|
|
4021
|
+
const outputPath = join7(artifactDir, "prefetch-output.json");
|
|
4022
|
+
const logPath = join7(artifactDir, "prefetch.log");
|
|
3406
4023
|
await writeJson(inputPath, payload);
|
|
3407
4024
|
const entrypoint = buildBundledPythonCommand(
|
|
3408
4025
|
"prefetch.py",
|
|
@@ -3439,7 +4056,7 @@ async function prefetchBaseModel(args) {
|
|
|
3439
4056
|
if (exitCode !== 0) {
|
|
3440
4057
|
throw new Error(`Model prefetch exited with code ${exitCode}. See ${logPath}.`);
|
|
3441
4058
|
}
|
|
3442
|
-
const output = JSON.parse(await
|
|
4059
|
+
const output = JSON.parse(await readFile6(outputPath, "utf8"));
|
|
3443
4060
|
const snapshotRevision = baseModelRevisionSchema.safeParse(output.snapshot_revision);
|
|
3444
4061
|
if (!snapshotRevision.success) {
|
|
3445
4062
|
throw new Error(
|
|
@@ -3502,6 +4119,346 @@ async function prefetchBaseModel(args) {
|
|
|
3502
4119
|
};
|
|
3503
4120
|
}
|
|
3504
4121
|
|
|
4122
|
+
// src/local-runtime/dataset-prefetch.ts
|
|
4123
|
+
import { createHash as createHash6 } from "crypto";
|
|
4124
|
+
import { createReadStream as createReadStream4 } from "fs";
|
|
4125
|
+
import { mkdir as mkdir7, readFile as readFile7, realpath as realpath3, stat as stat6 } from "fs/promises";
|
|
4126
|
+
import { createInterface } from "readline";
|
|
4127
|
+
import { basename, isAbsolute as isAbsolute5, join as join8, relative as relative4, resolve as resolve10, sep as sep5 } from "path";
|
|
4128
|
+
var SPLITS = ["training", "validation", "test"];
|
|
4129
|
+
var DatasetNotDownloadedError = class extends Error {
|
|
4130
|
+
constructor(message) {
|
|
4131
|
+
super(message);
|
|
4132
|
+
this.name = "DatasetNotDownloadedError";
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
function huggingFaceDataset(request) {
|
|
4136
|
+
const dataset = request.dataset_prebuilt;
|
|
4137
|
+
return dataset?.huggingface ? dataset : void 0;
|
|
4138
|
+
}
|
|
4139
|
+
function datasetFiles(dataset) {
|
|
4140
|
+
const files = {};
|
|
4141
|
+
for (const split of SPLITS) if (dataset[split]) files[split] = dataset[split];
|
|
4142
|
+
return files;
|
|
4143
|
+
}
|
|
4144
|
+
function huggingFaceHubCache(modelCache) {
|
|
4145
|
+
return withHuggingFaceCacheEnvironment(process.env, modelCache).HF_HUB_CACHE;
|
|
4146
|
+
}
|
|
4147
|
+
function repositoryDirectory(hubCache, repo) {
|
|
4148
|
+
return join8(hubCache, `datasets--${repo.replaceAll("/", "--")}`);
|
|
4149
|
+
}
|
|
4150
|
+
function isStrictDescendant(parent, child) {
|
|
4151
|
+
const path = relative4(parent, child);
|
|
4152
|
+
return path !== "" && path !== ".." && !path.startsWith(`..${sep5}`) && !isAbsolute5(path);
|
|
4153
|
+
}
|
|
4154
|
+
async function digestFile(path, size) {
|
|
4155
|
+
const sha256 = createHash6("sha256");
|
|
4156
|
+
const gitSha1 = createHash6("sha1").update(`blob ${size}\0`);
|
|
4157
|
+
for await (const chunk of createReadStream4(path)) {
|
|
4158
|
+
sha256.update(chunk);
|
|
4159
|
+
gitSha1.update(chunk);
|
|
4160
|
+
}
|
|
4161
|
+
return { sha256: sha256.digest("hex"), gitSha1: gitSha1.digest("hex") };
|
|
4162
|
+
}
|
|
4163
|
+
function describe(dataset) {
|
|
4164
|
+
return `${dataset.repo}@${dataset.revision ?? "main"}`;
|
|
4165
|
+
}
|
|
4166
|
+
async function resolveCachedHuggingFaceDataset(args) {
|
|
4167
|
+
const source = args.dataset.huggingface;
|
|
4168
|
+
const hubCache = huggingFaceHubCache(args.modelCache);
|
|
4169
|
+
const repository = repositoryDirectory(hubCache, source.repo);
|
|
4170
|
+
const prefetchHint = "Run `tt datasets prefetch` with this spec (for example `tt datasets prefetch tunedtensor.json`) to download it.";
|
|
4171
|
+
let revision = source.revision;
|
|
4172
|
+
if (!revision) {
|
|
4173
|
+
const ref = await readFile7(join8(repository, "refs", "main"), "utf8").catch(() => null);
|
|
4174
|
+
const parsed = baseModelRevisionSchema.safeParse(ref?.trim());
|
|
4175
|
+
if (!parsed.success) {
|
|
4176
|
+
throw new DatasetNotDownloadedError(
|
|
4177
|
+
`Hugging Face dataset ${describe(source)} is not in the local cache (${hubCache}). ${prefetchHint}`
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
revision = parsed.data;
|
|
4181
|
+
}
|
|
4182
|
+
const snapshot = join8(repository, "snapshots", revision);
|
|
4183
|
+
const physicalRepository = await realpath3(repository).catch(() => null);
|
|
4184
|
+
const blobs = physicalRepository ? join8(physicalRepository, "blobs") : null;
|
|
4185
|
+
const files = {};
|
|
4186
|
+
const localPaths = {};
|
|
4187
|
+
for (const [split, path] of Object.entries(datasetFiles(args.dataset))) {
|
|
4188
|
+
const local = join8(snapshot, ...path.split(/[/\\]/));
|
|
4189
|
+
const metadata = await stat6(local).catch(() => null);
|
|
4190
|
+
if (!metadata || !physicalRepository || !blobs) {
|
|
4191
|
+
throw new DatasetNotDownloadedError(
|
|
4192
|
+
`Hugging Face dataset file ${path} (${split}) from ${source.repo}@${revision} is not in the local cache (${hubCache}). ${prefetchHint}`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
const target = await realpath3(local);
|
|
4196
|
+
if (!metadata.isFile() || !isStrictDescendant(physicalRepository, target)) {
|
|
4197
|
+
throw new Error(`Cached dataset file ${local} must resolve inside its Hugging Face cache repository ${physicalRepository}.`);
|
|
4198
|
+
}
|
|
4199
|
+
if (metadata.size === 0) throw new Error(`Cached dataset file ${path} (${split}) is empty: ${local}`);
|
|
4200
|
+
const digests = await digestFile(target, metadata.size);
|
|
4201
|
+
const blobName = isStrictDescendant(blobs, target) ? basename(target).toLowerCase() : "";
|
|
4202
|
+
const expected = /^[0-9a-f]{64}$/.test(blobName) ? digests.sha256 : /^[0-9a-f]{40}$/.test(blobName) ? digests.gitSha1 : null;
|
|
4203
|
+
if (expected !== null && expected !== blobName) {
|
|
4204
|
+
throw new Error(
|
|
4205
|
+
`Cached dataset file ${path} (${split}) failed its checksum (${local}). Delete ${target} and run \`tt datasets prefetch\` again.`
|
|
4206
|
+
);
|
|
4207
|
+
}
|
|
4208
|
+
files[split] = { path, size_bytes: metadata.size, sha256: digests.sha256 };
|
|
4209
|
+
localPaths[split] = local;
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
source: datasetSourceSchema.parse({
|
|
4213
|
+
provider: "huggingface",
|
|
4214
|
+
repo: source.repo,
|
|
4215
|
+
requested_revision: source.revision ?? null,
|
|
4216
|
+
revision,
|
|
4217
|
+
pinned: Boolean(source.revision),
|
|
4218
|
+
columns: source.columns ?? null,
|
|
4219
|
+
snapshot_path: snapshot,
|
|
4220
|
+
files
|
|
4221
|
+
}),
|
|
4222
|
+
localPaths
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
function mappedField(row, name) {
|
|
4226
|
+
const value = row[name];
|
|
4227
|
+
if (typeof value === "string") return value.trim() ? value : null;
|
|
4228
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
async function readMappedRecords(path, split, columns) {
|
|
4232
|
+
const examples = [];
|
|
4233
|
+
let records = 0;
|
|
4234
|
+
let skipped = 0;
|
|
4235
|
+
const availableFields = /* @__PURE__ */ new Set();
|
|
4236
|
+
const lines = createInterface({ input: createReadStream4(path, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
4237
|
+
let index = -1;
|
|
4238
|
+
for await (const line of lines) {
|
|
4239
|
+
index += 1;
|
|
4240
|
+
if (!line.trim()) continue;
|
|
4241
|
+
records += 1;
|
|
4242
|
+
let value;
|
|
4243
|
+
try {
|
|
4244
|
+
value = JSON.parse(line);
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
throw new Error(`${split} line ${index + 1}: malformed JSON in ${path}`, { cause: error });
|
|
4247
|
+
}
|
|
4248
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4249
|
+
throw new Error(`${split} line ${index + 1}: expected a JSON object in ${path}`);
|
|
4250
|
+
}
|
|
4251
|
+
const row = value;
|
|
4252
|
+
for (const name of Object.keys(row)) availableFields.add(name);
|
|
4253
|
+
const input = mappedField(row, columns.input);
|
|
4254
|
+
const output = mappedField(row, columns.output);
|
|
4255
|
+
if (input === null || output === null) {
|
|
4256
|
+
skipped += 1;
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
examples.push({ input, output });
|
|
4260
|
+
}
|
|
4261
|
+
if (records > 0) {
|
|
4262
|
+
for (const name of [columns.input, columns.output]) {
|
|
4263
|
+
if (!availableFields.has(name)) {
|
|
4264
|
+
throw new Error(
|
|
4265
|
+
`${split} records have no "${name}" field (dataset_prebuilt.huggingface.columns). Available fields: ${[...availableFields].join(", ")}`
|
|
4266
|
+
);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
return { examples, records, skipped };
|
|
4271
|
+
}
|
|
4272
|
+
async function convertMappedDataset(args) {
|
|
4273
|
+
const system = buildSystemMessage(args.spec);
|
|
4274
|
+
const splits = Object.keys(args.resolved.localPaths);
|
|
4275
|
+
const key = createHash6("sha256").update(JSON.stringify({
|
|
4276
|
+
version: 1,
|
|
4277
|
+
sources: splits.map((split) => [split, args.resolved.source.files[split]?.sha256]),
|
|
4278
|
+
columns: args.columns,
|
|
4279
|
+
system
|
|
4280
|
+
})).digest("hex").slice(0, 16);
|
|
4281
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
4282
|
+
for (const split of splits) {
|
|
4283
|
+
loaded.set(split, await readMappedRecords(args.resolved.localPaths[split], split, args.columns));
|
|
4284
|
+
}
|
|
4285
|
+
const evaluationInputs = /* @__PURE__ */ new Set();
|
|
4286
|
+
const kept = /* @__PURE__ */ new Map();
|
|
4287
|
+
const conversion = {};
|
|
4288
|
+
for (const split of splits.filter((name) => name !== "training")) {
|
|
4289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4290
|
+
const data = loaded.get(split);
|
|
4291
|
+
const examples = data.examples.filter((example) => {
|
|
4292
|
+
const identity = datasetInputIdentity(example.input);
|
|
4293
|
+
if (seen.has(identity)) return false;
|
|
4294
|
+
seen.add(identity);
|
|
4295
|
+
evaluationInputs.add(identity);
|
|
4296
|
+
return true;
|
|
4297
|
+
});
|
|
4298
|
+
kept.set(split, examples);
|
|
4299
|
+
conversion[split] = {
|
|
4300
|
+
records: data.records,
|
|
4301
|
+
kept: examples.length,
|
|
4302
|
+
skipped_missing_fields: data.skipped,
|
|
4303
|
+
dropped_duplicate_inputs: data.examples.length - examples.length,
|
|
4304
|
+
dropped_eval_overlap: 0
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
const training = loaded.get("training");
|
|
4308
|
+
const trainingExamples = training.examples.filter((example) => !evaluationInputs.has(datasetInputIdentity(example.input)));
|
|
4309
|
+
kept.set("training", trainingExamples);
|
|
4310
|
+
conversion.training = {
|
|
4311
|
+
records: training.records,
|
|
4312
|
+
kept: trainingExamples.length,
|
|
4313
|
+
skipped_missing_fields: training.skipped,
|
|
4314
|
+
dropped_duplicate_inputs: 0,
|
|
4315
|
+
dropped_eval_overlap: training.examples.length - trainingExamples.length
|
|
4316
|
+
};
|
|
4317
|
+
await mkdir7(args.outputRoot, { recursive: true });
|
|
4318
|
+
const localPaths = {};
|
|
4319
|
+
for (const split of splits) {
|
|
4320
|
+
const examples = kept.get(split);
|
|
4321
|
+
if (examples.length === 0) {
|
|
4322
|
+
throw new Error(`No usable ${split} records remain after applying dataset_prebuilt.huggingface.columns.`);
|
|
4323
|
+
}
|
|
4324
|
+
const output = join8(args.outputRoot, `${split}-${key}.jsonl`);
|
|
4325
|
+
const content = `${examples.map((example) => JSON.stringify(exampleToChatRow(args.spec, example))).join("\n")}
|
|
4326
|
+
`;
|
|
4327
|
+
if (await readFile7(output, "utf8").catch(() => null) !== content) {
|
|
4328
|
+
await writeFileAtomic(output, content);
|
|
4329
|
+
}
|
|
4330
|
+
localPaths[split] = output;
|
|
4331
|
+
}
|
|
4332
|
+
return { localPaths, conversion };
|
|
4333
|
+
}
|
|
4334
|
+
async function resolveRequestDataset(request, config) {
|
|
4335
|
+
const dataset = huggingFaceDataset(request);
|
|
4336
|
+
if (!dataset) return request;
|
|
4337
|
+
const resolved = await resolveCachedHuggingFaceDataset({ dataset, modelCache: config.paths.modelCache });
|
|
4338
|
+
let localPaths = resolved.localPaths;
|
|
4339
|
+
let source = resolved.source;
|
|
4340
|
+
const columns = dataset.huggingface.columns;
|
|
4341
|
+
if (columns) {
|
|
4342
|
+
const converted = await convertMappedDataset({
|
|
4343
|
+
resolved,
|
|
4344
|
+
columns,
|
|
4345
|
+
spec: request.spec_snapshot,
|
|
4346
|
+
outputRoot: resolve10(config.artifactRoot, "datasets", "huggingface", safeName(dataset.huggingface.repo), resolved.source.revision)
|
|
4347
|
+
});
|
|
4348
|
+
localPaths = converted.localPaths;
|
|
4349
|
+
source = datasetSourceSchema.parse({ ...source, conversion: converted.conversion });
|
|
4350
|
+
}
|
|
4351
|
+
return fineTuneRunRequestSchema.parse({
|
|
4352
|
+
...request,
|
|
4353
|
+
dataset_prebuilt: {
|
|
4354
|
+
...localPaths,
|
|
4355
|
+
format: dataset.format
|
|
4356
|
+
},
|
|
4357
|
+
dataset_source: source
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
async function prefetchHuggingFaceDataset(args) {
|
|
4361
|
+
const dataset = huggingFaceDataset(args.request);
|
|
4362
|
+
if (!dataset) {
|
|
4363
|
+
throw new Error(
|
|
4364
|
+
"This spec has no dataset_prebuilt.huggingface source. Local dataset files need no download; set dataset_prebuilt.huggingface.repo to train on a Hugging Face dataset."
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
const modelCache = args.config.paths.modelCache ? resolve10(args.config.paths.modelCache) : void 0;
|
|
4368
|
+
const hubCache = huggingFaceHubCache(modelCache);
|
|
4369
|
+
if (args.localOnly) {
|
|
4370
|
+
const resolved2 = await resolveCachedHuggingFaceDataset({ dataset, modelCache });
|
|
4371
|
+
return {
|
|
4372
|
+
ok: true,
|
|
4373
|
+
status: "verified",
|
|
4374
|
+
local_files_only: true,
|
|
4375
|
+
hub_cache: hubCache,
|
|
4376
|
+
size_bytes: Object.values(resolved2.source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0),
|
|
4377
|
+
...resolved2.source
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
const artifactDir = resolve10(
|
|
4381
|
+
args.config.artifactRoot,
|
|
4382
|
+
"dataset-prefetch",
|
|
4383
|
+
`${safeName(dataset.huggingface.repo)}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`
|
|
4384
|
+
);
|
|
4385
|
+
await mkdir7(artifactDir, { recursive: true });
|
|
4386
|
+
const inputPath = join8(artifactDir, "prefetch-input.json");
|
|
4387
|
+
const outputPath = join8(artifactDir, "prefetch-output.json");
|
|
4388
|
+
const logPath = join8(artifactDir, "prefetch.log");
|
|
4389
|
+
await writeJson(inputPath, {
|
|
4390
|
+
repo: dataset.huggingface.repo,
|
|
4391
|
+
...dataset.huggingface.revision ? { revision: dataset.huggingface.revision } : {},
|
|
4392
|
+
files: datasetFiles(dataset),
|
|
4393
|
+
...modelCache ? { model_cache: modelCache } : {}
|
|
4394
|
+
});
|
|
4395
|
+
const entrypoint = buildBundledPythonCommand("prefetch_dataset.py", ["--input", inputPath, "--output", outputPath]);
|
|
4396
|
+
await args.reporter?.onEvent?.({
|
|
4397
|
+
stage: "dataset_prefetch",
|
|
4398
|
+
status: "running",
|
|
4399
|
+
message: "Downloading Hugging Face dataset files.",
|
|
4400
|
+
details: { repo: dataset.huggingface.repo, revision: dataset.huggingface.revision ?? "main", hub_cache: hubCache, log_path: logPath }
|
|
4401
|
+
});
|
|
4402
|
+
const { exitCode, stderr } = await runLoggedProcess({
|
|
4403
|
+
command: entrypoint.command,
|
|
4404
|
+
commandArgs: entrypoint.commandArgs,
|
|
4405
|
+
env: withBundledPythonEnvironment(withHuggingFaceCacheEnvironment(minimalMachineLearningEnvironment(process.env), modelCache)),
|
|
4406
|
+
logPath,
|
|
4407
|
+
reporter: args.reporter ? { ...args.reporter, verbose: true } : void 0,
|
|
4408
|
+
stage: "dataset_prefetch"
|
|
4409
|
+
});
|
|
4410
|
+
if (exitCode !== 0) {
|
|
4411
|
+
const reason = stderr.trim().split("\n").filter(Boolean).at(-1);
|
|
4412
|
+
throw new Error(
|
|
4413
|
+
`Dataset prefetch exited with code ${exitCode}${reason ? `: ${reason}` : ""}. See ${logPath}. Rerunning resumes from the files already downloaded.`
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
const output = JSON.parse(await readFile7(outputPath, "utf8"));
|
|
4417
|
+
const revision = baseModelRevisionSchema.parse(output.snapshot_revision);
|
|
4418
|
+
const resolved = await resolveCachedHuggingFaceDataset({
|
|
4419
|
+
dataset: { ...dataset, huggingface: { ...dataset.huggingface, revision } },
|
|
4420
|
+
modelCache
|
|
4421
|
+
});
|
|
4422
|
+
for (const [split, file] of Object.entries(resolved.source.files)) {
|
|
4423
|
+
if (output.files?.[split]?.sha256 !== file?.sha256) {
|
|
4424
|
+
throw new Error(`Dataset prefetch reported a different digest for ${split} than the cached file.`);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
const source = datasetSourceSchema.parse({
|
|
4428
|
+
...resolved.source,
|
|
4429
|
+
requested_revision: dataset.huggingface.revision ?? null,
|
|
4430
|
+
pinned: Boolean(dataset.huggingface.revision)
|
|
4431
|
+
});
|
|
4432
|
+
const sizeBytes = Object.values(source.files).reduce((total, file) => total + (file?.size_bytes ?? 0), 0);
|
|
4433
|
+
await args.reporter?.onEvent?.({
|
|
4434
|
+
stage: "dataset_prefetch",
|
|
4435
|
+
status: "completed",
|
|
4436
|
+
message: "Dataset files are available in the local Hugging Face cache.",
|
|
4437
|
+
details: { repo: source.repo, revision: source.revision, pinned: source.pinned, size_bytes: sizeBytes }
|
|
4438
|
+
});
|
|
4439
|
+
return {
|
|
4440
|
+
ok: true,
|
|
4441
|
+
status: "completed",
|
|
4442
|
+
local_files_only: false,
|
|
4443
|
+
hub_cache: hubCache,
|
|
4444
|
+
size_bytes: sizeBytes,
|
|
4445
|
+
...source,
|
|
4446
|
+
artifact_dir: artifactDir,
|
|
4447
|
+
log_uri: fileUri(logPath),
|
|
4448
|
+
command: entrypoint.displayCommand
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
async function ensureDatasetCached(args) {
|
|
4452
|
+
if (!huggingFaceDataset(args.request)) return { request: args.request, prefetch: null };
|
|
4453
|
+
try {
|
|
4454
|
+
return { request: await resolveRequestDataset(args.request, args.config), prefetch: null };
|
|
4455
|
+
} catch (error) {
|
|
4456
|
+
if (!(error instanceof DatasetNotDownloadedError) || args.config.dryRun) throw error;
|
|
4457
|
+
}
|
|
4458
|
+
const prefetch = await prefetchHuggingFaceDataset(args);
|
|
4459
|
+
return { request: await resolveRequestDataset(args.request, args.config), prefetch };
|
|
4460
|
+
}
|
|
4461
|
+
|
|
3505
4462
|
// src/local-runtime/general-regression.ts
|
|
3506
4463
|
function evaluateGeneralRegressionGate(comparison, policy) {
|
|
3507
4464
|
const failures = [];
|
|
@@ -3522,18 +4479,18 @@ function evaluateGeneralRegressionGate(comparison, policy) {
|
|
|
3522
4479
|
}
|
|
3523
4480
|
|
|
3524
4481
|
// src/local-runtime/orchestrator.ts
|
|
3525
|
-
import { lstat as lstat5, mkdir as
|
|
3526
|
-
import { createReadStream as
|
|
3527
|
-
import { createHash as
|
|
4482
|
+
import { lstat as lstat5, mkdir as mkdir11, open as open4, readFile as readFile10, readdir as readdir5, rename as rename4, rm as rm5, stat as stat9 } from "fs/promises";
|
|
4483
|
+
import { createReadStream as createReadStream5 } from "fs";
|
|
4484
|
+
import { createHash as createHash9, randomUUID as randomUUID6 } from "crypto";
|
|
3528
4485
|
import { performance } from "perf_hooks";
|
|
3529
4486
|
import { homedir as homedir4 } from "os";
|
|
3530
|
-
import { basename as
|
|
4487
|
+
import { basename as basename3, dirname as dirname9, join as join12, relative as relative5, resolve as resolve14 } from "path";
|
|
3531
4488
|
|
|
3532
4489
|
// src/local-runtime/gpu-executor.ts
|
|
3533
|
-
import { createHash as
|
|
3534
|
-
import { mkdtemp, mkdir as
|
|
4490
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
4491
|
+
import { mkdtemp, mkdir as mkdir8, realpath as realpath4, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
|
|
3535
4492
|
import { tmpdir } from "os";
|
|
3536
|
-
import { basename, dirname as dirname7, join as
|
|
4493
|
+
import { basename as basename2, dirname as dirname7, join as join9, resolve as resolve11 } from "path";
|
|
3537
4494
|
function shellQuote(value) {
|
|
3538
4495
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
3539
4496
|
}
|
|
@@ -3598,23 +4555,23 @@ async function checkAwsGpu(gpu) {
|
|
|
3598
4555
|
if (result.exitCode !== 0) throw new Error(`AWS GPU preflight failed: ${result.stderr}`);
|
|
3599
4556
|
}
|
|
3600
4557
|
function gpuBaseModelPath(config, model, revision) {
|
|
3601
|
-
if (config.paths.baseModel) return
|
|
4558
|
+
if (config.paths.baseModel) return resolve11(config.paths.baseModel);
|
|
3602
4559
|
if (!revision || !/^[a-f0-9]{40}$/i.test(revision)) {
|
|
3603
4560
|
throw new Error("AWS GPU execution needs an immutable base-model revision. Run tt models prefetch first or configure paths.baseModel.");
|
|
3604
4561
|
}
|
|
3605
4562
|
const env = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
|
|
3606
|
-
return
|
|
4563
|
+
return join9(env.HF_HUB_CACHE, `models--${model.replaceAll("/", "--")}`, "snapshots", revision);
|
|
3607
4564
|
}
|
|
3608
4565
|
async function physicalTransferPath(path) {
|
|
3609
4566
|
try {
|
|
3610
|
-
return await
|
|
4567
|
+
return await realpath4(path);
|
|
3611
4568
|
} catch (error) {
|
|
3612
4569
|
if (error.code !== "ENOENT" || dirname7(path) === path) throw error;
|
|
3613
|
-
return
|
|
4570
|
+
return join9(await physicalTransferPath(dirname7(path)), basename2(path));
|
|
3614
4571
|
}
|
|
3615
4572
|
}
|
|
3616
4573
|
async function assertSeparateTransferPaths(files) {
|
|
3617
|
-
const paths = await Promise.all(files.map((file) => physicalTransferPath(
|
|
4574
|
+
const paths = await Promise.all(files.map((file) => physicalTransferPath(resolve11(file.path))));
|
|
3618
4575
|
for (let i = 0; i < files.length; i += 1) {
|
|
3619
4576
|
for (let j = 0; j < i; j += 1) {
|
|
3620
4577
|
if (files[i].direction === "input" && files[j].direction === "input") continue;
|
|
@@ -3631,13 +4588,13 @@ async function runGpuProcess(args) {
|
|
|
3631
4588
|
await assertSeparateTransferPaths(args.files);
|
|
3632
4589
|
const host = await resolveGpuHost(args.gpu);
|
|
3633
4590
|
const options = sshOptions(args.gpu);
|
|
3634
|
-
const identity = args.logPath ?
|
|
4591
|
+
const identity = args.logPath ? createHash7("sha256").update(resolve11(args.logPath)).digest("hex").slice(0, 32) : randomUUID5();
|
|
3635
4592
|
const root = `/tmp/tt-gpu-${identity}`;
|
|
3636
|
-
const scratch = await mkdtemp(
|
|
3637
|
-
const runtime =
|
|
3638
|
-
const mappings = args.files.map((file, index) => ({ ...file, path:
|
|
4593
|
+
const scratch = await mkdtemp(join9(tmpdir(), "tt-gpu-"));
|
|
4594
|
+
const runtime = join9(localRuntimePackageRoot(import.meta.url), "training", args.runtime);
|
|
4595
|
+
const mappings = args.files.map((file, index) => ({ ...file, path: resolve11(file.path), remote: `${root}/files/${index}/${basename2(file.path)}` }));
|
|
3639
4596
|
const mapPath = (path) => {
|
|
3640
|
-
const absolute =
|
|
4597
|
+
const absolute = resolve11(path);
|
|
3641
4598
|
const match = [...mappings].sort((a, b) => b.path.length - a.path.length).find((file) => absolute === file.path || file.directory && absolute.startsWith(`${file.path}/`));
|
|
3642
4599
|
if (match) return match.remote + absolute.slice(match.path.length);
|
|
3643
4600
|
if (absolute === runtime || absolute.startsWith(`${runtime}/`)) return `${root}/runtime${absolute.slice(runtime.length)}`;
|
|
@@ -3675,7 +4632,7 @@ async function runGpuProcess(args) {
|
|
|
3675
4632
|
};
|
|
3676
4633
|
const grace = Math.ceil((args.shutdownGraceMs ?? 5e3) / 1e3);
|
|
3677
4634
|
const stop = `kill -TERM -- -"$pid" 2>/dev/null || true; remaining=${grace}; while kill -0 -- -"$pid" 2>/dev/null && [ "$remaining" -gt 0 ]; do sleep 1; remaining=$((remaining - 1)); done; kill -KILL -- -"$pid" 2>/dev/null || true`;
|
|
3678
|
-
const owner =
|
|
4635
|
+
const owner = randomUUID5();
|
|
3679
4636
|
let reservationAttempted = false;
|
|
3680
4637
|
let started = false;
|
|
3681
4638
|
let completed = false;
|
|
@@ -3686,12 +4643,12 @@ async function runGpuProcess(args) {
|
|
|
3686
4643
|
throw new Error(`Cannot reserve GPU staging directory ${host}:${root}. Another process or unrecovered artifacts may be present. ${error instanceof Error ? error.message : error}`);
|
|
3687
4644
|
});
|
|
3688
4645
|
for (const name of ["pyproject.toml", "uv.lock", "src"]) {
|
|
3689
|
-
await sync(
|
|
4646
|
+
await sync(join9(runtime, name), `${root}/runtime/`, true);
|
|
3690
4647
|
}
|
|
3691
4648
|
for (const file of mappings) {
|
|
3692
4649
|
await ssh(`mkdir -p ${shellQuote(file.directory ? file.remote : dirname7(file.remote))}`);
|
|
3693
4650
|
if (file.direction !== "output") {
|
|
3694
|
-
const exists3 = await
|
|
4651
|
+
const exists3 = await stat7(file.path).catch((error) => {
|
|
3695
4652
|
if (error.code === "ENOENT" && file.direction === "both") return void 0;
|
|
3696
4653
|
throw error;
|
|
3697
4654
|
});
|
|
@@ -3701,8 +4658,8 @@ async function runGpuProcess(args) {
|
|
|
3701
4658
|
if (args.document) {
|
|
3702
4659
|
const value = { ...args.document.value };
|
|
3703
4660
|
for (const key of args.document.pathKeys) if (typeof value[key] === "string") value[key] = mapPath(value[key]);
|
|
3704
|
-
const path =
|
|
3705
|
-
await
|
|
4661
|
+
const path = join9(scratch, "document.json");
|
|
4662
|
+
await writeFile5(path, JSON.stringify(value), { mode: 384 });
|
|
3706
4663
|
await sync(path, mapPath(args.document.path), true);
|
|
3707
4664
|
}
|
|
3708
4665
|
const env = {
|
|
@@ -3734,7 +4691,7 @@ async function runGpuProcess(args) {
|
|
|
3734
4691
|
if (started) {
|
|
3735
4692
|
await ssh(`bash -c ${shellQuote(`if test -f ${root}/pid; then pid=$(cat ${root}/pid); ${stop}; fi`)}`, false);
|
|
3736
4693
|
for (const file of mappings.filter((file2) => file2.direction !== "input")) {
|
|
3737
|
-
await
|
|
4694
|
+
await mkdir8(file.directory ? file.path : dirname7(file.path), { recursive: true });
|
|
3738
4695
|
if (!completed && !file.directory) {
|
|
3739
4696
|
const exists3 = await runLoggedProcess({ command: "ssh", commandArgs: [...options, host, `test -e ${shellQuote(file.remote)}`], stage: args.stage, timeoutMs: 3e4 });
|
|
3740
4697
|
if (exists3.exitCode === 1) continue;
|
|
@@ -3747,15 +4704,15 @@ async function runGpuProcess(args) {
|
|
|
3747
4704
|
} catch (error) {
|
|
3748
4705
|
throw new Error(`GPU cleanup or artifact retrieval failed. Remote files remain at ${host}:${root}. ${error instanceof Error ? error.message : error}`);
|
|
3749
4706
|
} finally {
|
|
3750
|
-
await
|
|
4707
|
+
await rm3(scratch, { recursive: true, force: true });
|
|
3751
4708
|
}
|
|
3752
4709
|
}
|
|
3753
4710
|
}
|
|
3754
4711
|
|
|
3755
4712
|
// src/local-runtime/evaluation.ts
|
|
3756
|
-
import { createHash as
|
|
3757
|
-
import { mkdir as
|
|
3758
|
-
import { dirname as dirname8, join as
|
|
4713
|
+
import { createHash as createHash8 } from "crypto";
|
|
4714
|
+
import { mkdir as mkdir9, readFile as readFile8, rm as rm4, stat as stat8, writeFile as writeFile6 } from "fs/promises";
|
|
4715
|
+
import { dirname as dirname8, join as join10, resolve as resolve12 } from "path";
|
|
3759
4716
|
function normalize(value) {
|
|
3760
4717
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
3761
4718
|
}
|
|
@@ -3849,11 +4806,11 @@ function extractJsonObject(value) {
|
|
|
3849
4806
|
}
|
|
3850
4807
|
return null;
|
|
3851
4808
|
}
|
|
3852
|
-
function
|
|
3853
|
-
if (Array.isArray(value)) return value.map(
|
|
4809
|
+
function canonicalJson4(value) {
|
|
4810
|
+
if (Array.isArray(value)) return value.map(canonicalJson4);
|
|
3854
4811
|
if (value && typeof value === "object") {
|
|
3855
4812
|
return Object.fromEntries(
|
|
3856
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key,
|
|
4813
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson4(item)])
|
|
3857
4814
|
);
|
|
3858
4815
|
}
|
|
3859
4816
|
return value;
|
|
@@ -3879,7 +4836,7 @@ function scoreJsonFields(expected, actual, configuredFields) {
|
|
|
3879
4836
|
const schemaMatch = Boolean(actualJson) && expectedKeys.length === actualKeys.length && expectedKeys.every((key, index) => key === actualKeys[index]);
|
|
3880
4837
|
const fieldResults = {};
|
|
3881
4838
|
for (const field of fields) {
|
|
3882
|
-
fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(
|
|
4839
|
+
fieldResults[field] = Object.prototype.hasOwnProperty.call(expectedJson, field) && Boolean(actualJson) && JSON.stringify(canonicalJson4(expectedJson[field])) === JSON.stringify(canonicalJson4(actualJson?.[field]));
|
|
3883
4840
|
}
|
|
3884
4841
|
const correct = Object.values(fieldResults).filter(Boolean).length;
|
|
3885
4842
|
const score = fields.length > 0 ? correct / fields.length : 0;
|
|
@@ -3975,8 +4932,8 @@ async function runTransformersInference(args) {
|
|
|
3975
4932
|
const inputPath = `${args.outputPath}.inference-input.json`;
|
|
3976
4933
|
const outputPath = `${args.outputPath}.inference-output.json`;
|
|
3977
4934
|
const logPath = `${args.outputPath}.inference.log`;
|
|
3978
|
-
await
|
|
3979
|
-
await
|
|
4935
|
+
await mkdir9(dirname8(inputPath), { recursive: true });
|
|
4936
|
+
await writeFile6(inputPath, `${JSON.stringify({
|
|
3980
4937
|
protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
3981
4938
|
kind: args.kind,
|
|
3982
4939
|
model_id: args.modelId,
|
|
@@ -3990,7 +4947,7 @@ async function runTransformersInference(args) {
|
|
|
3990
4947
|
id: String(index),
|
|
3991
4948
|
input: example.input
|
|
3992
4949
|
})),
|
|
3993
|
-
model_cache: args.config.paths.modelCache ?
|
|
4950
|
+
model_cache: args.config.paths.modelCache ? resolve12(args.config.paths.modelCache) : void 0,
|
|
3994
4951
|
trust_remote_code: false,
|
|
3995
4952
|
device: args.config.evaluation.inference.device,
|
|
3996
4953
|
generation: {
|
|
@@ -4000,7 +4957,7 @@ async function runTransformersInference(args) {
|
|
|
4000
4957
|
}
|
|
4001
4958
|
}, null, 2)}
|
|
4002
4959
|
`, "utf8");
|
|
4003
|
-
await
|
|
4960
|
+
await rm4(outputPath, { force: true });
|
|
4004
4961
|
const entrypoint = buildBundledPythonCommand(
|
|
4005
4962
|
"evaluate.py",
|
|
4006
4963
|
["--input", inputPath, "--output", outputPath]
|
|
@@ -4018,7 +4975,7 @@ async function runTransformersInference(args) {
|
|
|
4018
4975
|
});
|
|
4019
4976
|
const gpu = args.config.evaluation.inference.device === "cuda" ? args.config.gpu : void 0;
|
|
4020
4977
|
const execute = gpu ? async (processArgs) => {
|
|
4021
|
-
const document = JSON.parse(await
|
|
4978
|
+
const document = JSON.parse(await readFile8(inputPath, "utf8"));
|
|
4022
4979
|
document.model_source = gpuBaseModelPath(args.config, args.baseModelId, args.baseModelRevision);
|
|
4023
4980
|
delete document.model_cache;
|
|
4024
4981
|
const files = [
|
|
@@ -4028,7 +4985,7 @@ async function runTransformersInference(args) {
|
|
|
4028
4985
|
...document.adapter_path ? [{
|
|
4029
4986
|
path: document.adapter_path,
|
|
4030
4987
|
direction: "input",
|
|
4031
|
-
directory: (await
|
|
4988
|
+
directory: (await stat8(document.adapter_path)).isDirectory()
|
|
4032
4989
|
}] : []
|
|
4033
4990
|
];
|
|
4034
4991
|
return runGpuProcess({
|
|
@@ -4062,7 +5019,7 @@ async function runTransformersInference(args) {
|
|
|
4062
5019
|
}
|
|
4063
5020
|
let output;
|
|
4064
5021
|
try {
|
|
4065
|
-
output = JSON.parse(await
|
|
5022
|
+
output = JSON.parse(await readFile8(outputPath, "utf8"));
|
|
4066
5023
|
} catch (error) {
|
|
4067
5024
|
throw new Error(
|
|
4068
5025
|
`Transformers inference did not write valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -4079,7 +5036,7 @@ function assertEvaluationScoringReady(config) {
|
|
|
4079
5036
|
}
|
|
4080
5037
|
}
|
|
4081
5038
|
function baselineCacheKey(args) {
|
|
4082
|
-
return
|
|
5039
|
+
return createHash8("sha256").update(JSON.stringify({
|
|
4083
5040
|
v: 6,
|
|
4084
5041
|
inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
4085
5042
|
package_version: args.packageVersion,
|
|
@@ -4099,15 +5056,15 @@ function baselineInputsAreStable(args) {
|
|
|
4099
5056
|
return args.config.paths.baseModel ? Boolean(args.sourceFingerprint) : Boolean(args.baseModelRevision);
|
|
4100
5057
|
}
|
|
4101
5058
|
function baselineStoreRoot(config) {
|
|
4102
|
-
return config.storeRoot ?
|
|
5059
|
+
return config.storeRoot ? resolve12(config.storeRoot) : defaultLocalHome();
|
|
4103
5060
|
}
|
|
4104
5061
|
function baselineCachePath(config, key) {
|
|
4105
|
-
return
|
|
5062
|
+
return join10(baselineStoreRoot(config), "cache", "baseline-evals", `${key}.json`);
|
|
4106
5063
|
}
|
|
4107
5064
|
async function readBaselineCache(config, path) {
|
|
4108
5065
|
await hardenExistingLocalStore(baselineStoreRoot(config));
|
|
4109
5066
|
try {
|
|
4110
|
-
return evalReportSchema.parse(JSON.parse(await
|
|
5067
|
+
return evalReportSchema.parse(JSON.parse(await readFile8(path, "utf8")));
|
|
4111
5068
|
} catch {
|
|
4112
5069
|
return null;
|
|
4113
5070
|
}
|
|
@@ -4116,7 +5073,7 @@ var cachedPackageVersion = null;
|
|
|
4116
5073
|
async function packageVersion() {
|
|
4117
5074
|
if (cachedPackageVersion !== null) return cachedPackageVersion;
|
|
4118
5075
|
try {
|
|
4119
|
-
const raw = await
|
|
5076
|
+
const raw = await readFile8(resolve12(localRuntimePackageRoot(import.meta.url), "package.json"), "utf8");
|
|
4120
5077
|
cachedPackageVersion = String(
|
|
4121
5078
|
JSON.parse(raw).version ?? "unknown"
|
|
4122
5079
|
);
|
|
@@ -4185,7 +5142,7 @@ async function evaluateExamples(args) {
|
|
|
4185
5142
|
baseModelId,
|
|
4186
5143
|
args.baseModelRevision
|
|
4187
5144
|
);
|
|
4188
|
-
const localModelSource = args.modelSource ?
|
|
5145
|
+
const localModelSource = args.modelSource ? resolve12(args.modelSource) !== resolve12(baseModelId) : false;
|
|
4189
5146
|
const baseModelRevision = localModelSource ? defaultBaseModelRevision(baseModelId) : requestedBaseModelRevision;
|
|
4190
5147
|
const maxExamples = args.config.evaluation.maxExamples ?? args.maxExamples ?? args.examples.length;
|
|
4191
5148
|
const truncated = args.examples.length > maxExamples;
|
|
@@ -4301,8 +5258,8 @@ function compareEvalReports(baseline, candidate) {
|
|
|
4301
5258
|
}
|
|
4302
5259
|
|
|
4303
5260
|
// src/local-runtime/process-training.ts
|
|
4304
|
-
import { access, mkdir as
|
|
4305
|
-
import { join as
|
|
5261
|
+
import { access, mkdir as mkdir10, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
|
|
5262
|
+
import { join as join11, resolve as resolve13 } from "path";
|
|
4306
5263
|
function buildTrainingHyperparameters(request, options = {}) {
|
|
4307
5264
|
const model = resolveTrainingModel(request.spec_snapshot.base_model);
|
|
4308
5265
|
const hyper = request.hyperparameters;
|
|
@@ -4393,15 +5350,15 @@ function createTrainingProgressForwarder(reporter) {
|
|
|
4393
5350
|
async function launchProcessTraining(args) {
|
|
4394
5351
|
const { request, artifacts, config } = args;
|
|
4395
5352
|
const jobName = `tt-local-${request.run_id}`;
|
|
4396
|
-
await
|
|
5353
|
+
await mkdir10(artifacts.trainingDir, { recursive: true });
|
|
4397
5354
|
await copyDatasetToTrainingChannel(artifacts);
|
|
4398
5355
|
const hyperparameters = buildTrainingHyperparameters(request, {
|
|
4399
5356
|
baseModelRevision: args.baseModelRevision
|
|
4400
5357
|
});
|
|
4401
|
-
await writeJson(
|
|
5358
|
+
await writeJson(join11(artifacts.trainingConfigDir, "hyperparameters.json"), hyperparameters);
|
|
4402
5359
|
const entrypoint = buildBundledPythonCommand("train.py");
|
|
4403
5360
|
if (config.dryRun) {
|
|
4404
|
-
await
|
|
5361
|
+
await writeFile7(
|
|
4405
5362
|
artifacts.trainingLog,
|
|
4406
5363
|
"Dry run enabled. Training process was not launched.\n",
|
|
4407
5364
|
"utf8"
|
|
@@ -4423,11 +5380,11 @@ async function launchProcessTraining(args) {
|
|
|
4423
5380
|
withOfflineHuggingFaceCacheEnvironment({
|
|
4424
5381
|
...inheritedEnv,
|
|
4425
5382
|
BACKEND: "local",
|
|
4426
|
-
SM_CHANNEL_TRAINING:
|
|
4427
|
-
SM_CHANNEL_BASE_MODEL: config.paths.baseModel ?
|
|
4428
|
-
SM_MODEL_DIR:
|
|
4429
|
-
SM_OUTPUT_DIR:
|
|
4430
|
-
TT_HYPERPARAMETERS_PATH:
|
|
5383
|
+
SM_CHANNEL_TRAINING: resolve13(artifacts.trainingInputDir),
|
|
5384
|
+
SM_CHANNEL_BASE_MODEL: config.paths.baseModel ? resolve13(config.paths.baseModel) : void 0,
|
|
5385
|
+
SM_MODEL_DIR: resolve13(artifacts.trainingModelDir),
|
|
5386
|
+
SM_OUTPUT_DIR: resolve13(artifacts.trainingOutputDir),
|
|
5387
|
+
TT_HYPERPARAMETERS_PATH: resolve13(artifacts.trainingConfigDir, "hyperparameters.json")
|
|
4431
5388
|
}, config.paths.modelCache)
|
|
4432
5389
|
);
|
|
4433
5390
|
for (const [key, value] of Object.entries(childEnv)) {
|
|
@@ -4435,10 +5392,10 @@ async function launchProcessTraining(args) {
|
|
|
4435
5392
|
}
|
|
4436
5393
|
const remoteBaseModel = config.gpu ? gpuBaseModelPath(config, request.spec_snapshot.base_model, args.baseModelRevision) : void 0;
|
|
4437
5394
|
if (remoteBaseModel) childEnv.SM_CHANNEL_BASE_MODEL = remoteBaseModel;
|
|
4438
|
-
if (config.paths.modelCache) await
|
|
5395
|
+
if (config.paths.modelCache) await mkdir10(resolve13(config.paths.modelCache), { recursive: true });
|
|
4439
5396
|
if (config.paths.baseModel) {
|
|
4440
5397
|
await verifyLocalBaseModel(
|
|
4441
|
-
|
|
5398
|
+
resolve13(config.paths.baseModel),
|
|
4442
5399
|
request.spec_snapshot.base_model,
|
|
4443
5400
|
config.paths.modelCache
|
|
4444
5401
|
);
|
|
@@ -4477,12 +5434,12 @@ async function launchProcessTraining(args) {
|
|
|
4477
5434
|
onLine: (line) => forwardTrainingProgress(line),
|
|
4478
5435
|
shouldCancel: args.shouldCancel
|
|
4479
5436
|
});
|
|
4480
|
-
const metricsPath =
|
|
4481
|
-
const metrics = await pathExists(metricsPath) ? JSON.parse(await
|
|
5437
|
+
const metricsPath = join11(artifacts.trainingModelDir, "training-metrics.json");
|
|
5438
|
+
const metrics = await pathExists(metricsPath) ? JSON.parse(await readFile9(metricsPath, "utf8")) : null;
|
|
4482
5439
|
if (exitCode !== 0) {
|
|
4483
5440
|
throw new Error(`Training process exited with code ${exitCode}. See ${artifacts.trainingLog}.`);
|
|
4484
5441
|
}
|
|
4485
|
-
const modelTarPath =
|
|
5442
|
+
const modelTarPath = join11(artifacts.trainingOutputDir, "model.tar.gz");
|
|
4486
5443
|
const modelUri = await pathExists(modelTarPath) ? fileUri(modelTarPath) : fileUri(artifacts.trainingModelDir);
|
|
4487
5444
|
await args.reporter?.onEvent?.({
|
|
4488
5445
|
stage: "training",
|
|
@@ -4577,7 +5534,7 @@ function validateLocalPipeline(pipeline) {
|
|
|
4577
5534
|
return pipeline;
|
|
4578
5535
|
}
|
|
4579
5536
|
async function loadJsonFile(path) {
|
|
4580
|
-
return JSON.parse(await
|
|
5537
|
+
return JSON.parse(await readFile10(path, "utf8"));
|
|
4581
5538
|
}
|
|
4582
5539
|
async function loadRunRequest(path) {
|
|
4583
5540
|
return fineTuneRunRequestSchema.parse(await loadJsonFile(path));
|
|
@@ -4585,13 +5542,13 @@ async function loadRunRequest(path) {
|
|
|
4585
5542
|
function parseLocalRunnerConfig(document, path) {
|
|
4586
5543
|
const config = localRunnerConfigSchema.parse(document);
|
|
4587
5544
|
if (!path) return config;
|
|
4588
|
-
const configPath =
|
|
5545
|
+
const configPath = resolve14(path);
|
|
4589
5546
|
const base = dirname9(configPath);
|
|
4590
5547
|
const configPathValue = (value) => {
|
|
4591
5548
|
if (!value) return void 0;
|
|
4592
5549
|
if (value === "~") return homedir4();
|
|
4593
|
-
if (value.startsWith("~/")) return
|
|
4594
|
-
return
|
|
5550
|
+
if (value.startsWith("~/")) return resolve14(homedir4(), value.slice(2));
|
|
5551
|
+
return resolve14(base, value);
|
|
4595
5552
|
};
|
|
4596
5553
|
return {
|
|
4597
5554
|
...config,
|
|
@@ -4613,7 +5570,7 @@ function parseLocalRunnerConfig(document, path) {
|
|
|
4613
5570
|
}
|
|
4614
5571
|
async function loadLocalRunnerConfig(path) {
|
|
4615
5572
|
if (!path) return parseLocalRunnerConfig({});
|
|
4616
|
-
const configPath =
|
|
5573
|
+
const configPath = resolve14(path);
|
|
4617
5574
|
return parseLocalRunnerConfig(
|
|
4618
5575
|
await loadJsonFile(configPath),
|
|
4619
5576
|
configPath
|
|
@@ -4628,7 +5585,7 @@ function stripFileUri(path) {
|
|
|
4628
5585
|
}
|
|
4629
5586
|
async function pathExists2(path) {
|
|
4630
5587
|
try {
|
|
4631
|
-
await
|
|
5588
|
+
await stat9(path);
|
|
4632
5589
|
return true;
|
|
4633
5590
|
} catch {
|
|
4634
5591
|
return false;
|
|
@@ -4672,7 +5629,7 @@ function addDryRunPlaceholders(request, dryRun) {
|
|
|
4672
5629
|
};
|
|
4673
5630
|
}
|
|
4674
5631
|
async function validateDatasetInputs(request, config) {
|
|
4675
|
-
const inputIdentity =
|
|
5632
|
+
const inputIdentity = datasetInputIdentity;
|
|
4676
5633
|
const evalSampleSeed = config.evaluation.sampleSeed ?? deriveSampleSeed(request.behavior_spec_id);
|
|
4677
5634
|
if (!request.dataset_prebuilt) {
|
|
4678
5635
|
const inlineExamples = examplesFromSpec(request.spec_snapshot);
|
|
@@ -4762,7 +5719,10 @@ async function validateDatasetInputs(request, config) {
|
|
|
4762
5719
|
}
|
|
4763
5720
|
async function validateLocalFineTuneInput(input) {
|
|
4764
5721
|
const config = localRunnerConfigSchema.parse(input.config);
|
|
4765
|
-
const request =
|
|
5722
|
+
const request = await resolveRequestDataset(
|
|
5723
|
+
fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun)),
|
|
5724
|
+
config
|
|
5725
|
+
);
|
|
4766
5726
|
await validateDatasetInputs(request, config);
|
|
4767
5727
|
if (config.evaluation.generalRegression) {
|
|
4768
5728
|
await evaluationSuiteFromChatJsonl(
|
|
@@ -4779,14 +5739,14 @@ function artifactPrefix(request) {
|
|
|
4779
5739
|
runId: request.run_id
|
|
4780
5740
|
});
|
|
4781
5741
|
}
|
|
4782
|
-
function
|
|
5742
|
+
function canonicalJson5(value) {
|
|
4783
5743
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
4784
|
-
if (Array.isArray(value)) return `[${value.map((entry) =>
|
|
5744
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson5(entry)).join(",")}]`;
|
|
4785
5745
|
const object = value;
|
|
4786
|
-
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${
|
|
5746
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson5(object[key])}`).join(",")}}`;
|
|
4787
5747
|
}
|
|
4788
5748
|
function hashJson(value) {
|
|
4789
|
-
return
|
|
5749
|
+
return createHash9("sha256").update(canonicalJson5(value)).digest("hex");
|
|
4790
5750
|
}
|
|
4791
5751
|
var packageRoot2 = localRuntimePackageRoot(import.meta.url);
|
|
4792
5752
|
async function hashFileIfPresent(path) {
|
|
@@ -4798,21 +5758,21 @@ async function hashFileIfPresent(path) {
|
|
|
4798
5758
|
}
|
|
4799
5759
|
async function packageVersion2() {
|
|
4800
5760
|
try {
|
|
4801
|
-
const parsed = JSON.parse(await
|
|
5761
|
+
const parsed = JSON.parse(await readFile10(resolve14(packageRoot2, "package.json"), "utf8"));
|
|
4802
5762
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4803
5763
|
} catch {
|
|
4804
5764
|
return "unknown";
|
|
4805
5765
|
}
|
|
4806
5766
|
}
|
|
4807
5767
|
async function runtimeFingerprint() {
|
|
4808
|
-
const bundledProject2 =
|
|
5768
|
+
const bundledProject2 = resolve14(packageRoot2, "training/adapter");
|
|
4809
5769
|
return hashJson({
|
|
4810
5770
|
tt_local_version: await packageVersion2(),
|
|
4811
5771
|
node_version: process.version,
|
|
4812
5772
|
platform: process.platform,
|
|
4813
5773
|
architecture: process.arch,
|
|
4814
|
-
runner_pyproject: await hashFileIfPresent(
|
|
4815
|
-
runner_uv_lock: await hashFileIfPresent(
|
|
5774
|
+
runner_pyproject: await hashFileIfPresent(resolve14(bundledProject2, "pyproject.toml")),
|
|
5775
|
+
runner_uv_lock: await hashFileIfPresent(resolve14(bundledProject2, "uv.lock"))
|
|
4816
5776
|
});
|
|
4817
5777
|
}
|
|
4818
5778
|
function preparedSourceFingerprint(args) {
|
|
@@ -4826,8 +5786,8 @@ function preparedSourceFingerprint(args) {
|
|
|
4826
5786
|
});
|
|
4827
5787
|
}
|
|
4828
5788
|
async function hashFile(path) {
|
|
4829
|
-
const hash =
|
|
4830
|
-
for await (const chunk of
|
|
5789
|
+
const hash = createHash9("sha256");
|
|
5790
|
+
for await (const chunk of createReadStream5(path)) hash.update(chunk);
|
|
4831
5791
|
return hash.digest("hex");
|
|
4832
5792
|
}
|
|
4833
5793
|
async function datasetFingerprints(request) {
|
|
@@ -4859,9 +5819,9 @@ async function resolveBaseModelRevision(request, config) {
|
|
|
4859
5819
|
}
|
|
4860
5820
|
const repository = `models--${request.spec_snapshot.base_model.replaceAll("/", "--")}`;
|
|
4861
5821
|
const cacheEnvironment = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
|
|
4862
|
-
const refPath =
|
|
5822
|
+
const refPath = resolve14(cacheEnvironment.HF_HUB_CACHE, repository, "refs", "main");
|
|
4863
5823
|
try {
|
|
4864
|
-
const revision = (await
|
|
5824
|
+
const revision = (await readFile10(refPath, "utf8")).trim();
|
|
4865
5825
|
if (revision) return revision;
|
|
4866
5826
|
} catch {
|
|
4867
5827
|
}
|
|
@@ -4880,18 +5840,18 @@ async function fingerprintLocalBaseModel(uri, expectedModelId, modelCache) {
|
|
|
4880
5840
|
const files = [];
|
|
4881
5841
|
const visit = async (directory) => {
|
|
4882
5842
|
for (const entry of await readdir5(directory, { withFileTypes: true })) {
|
|
4883
|
-
const child =
|
|
5843
|
+
const child = resolve14(directory, entry.name);
|
|
4884
5844
|
if (entry.isDirectory()) {
|
|
4885
5845
|
await visit(child);
|
|
4886
5846
|
continue;
|
|
4887
5847
|
}
|
|
4888
|
-
const childMetadata = await
|
|
5848
|
+
const childMetadata = await stat9(child);
|
|
4889
5849
|
if (entry.isSymbolicLink() && !childMetadata.isFile()) {
|
|
4890
5850
|
throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
|
|
4891
5851
|
}
|
|
4892
5852
|
if (!childMetadata.isFile()) continue;
|
|
4893
5853
|
files.push({
|
|
4894
|
-
path:
|
|
5854
|
+
path: relative5(root, child).split("\\").join("/"),
|
|
4895
5855
|
size_bytes: childMetadata.size,
|
|
4896
5856
|
sha256: await hashFile(child)
|
|
4897
5857
|
});
|
|
@@ -4920,26 +5880,26 @@ async function readStageMetadata(path) {
|
|
|
4920
5880
|
}
|
|
4921
5881
|
async function removePrefixedArtifacts(path) {
|
|
4922
5882
|
const directory = dirname9(path);
|
|
4923
|
-
const prefix = `${
|
|
5883
|
+
const prefix = `${basename3(path)}.`;
|
|
4924
5884
|
const names = await readdir5(directory).catch(() => []);
|
|
4925
5885
|
await Promise.all([
|
|
4926
|
-
|
|
4927
|
-
...names.filter((name) => name.startsWith(prefix)).map((name) =>
|
|
5886
|
+
rm5(path, { force: true }),
|
|
5887
|
+
...names.filter((name) => name.startsWith(prefix)).map((name) => rm5(resolve14(directory, name), { recursive: true, force: true }))
|
|
4928
5888
|
]);
|
|
4929
5889
|
}
|
|
4930
5890
|
async function cleanupStageArtifacts(artifacts, stage) {
|
|
4931
|
-
const removeReport = async () =>
|
|
5891
|
+
const removeReport = async () => rm5(artifacts.runReportJson, { force: true });
|
|
4932
5892
|
if (stage === "prepare") {
|
|
4933
5893
|
await Promise.all([
|
|
4934
5894
|
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
4935
5895
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
4936
5896
|
removePrefixedArtifacts(artifacts.generalBaselineEvalJson),
|
|
4937
5897
|
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
4938
|
-
|
|
5898
|
+
rm5(artifacts.trainingDir, { recursive: true, force: true }),
|
|
4939
5899
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
4940
|
-
|
|
5900
|
+
rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
4941
5901
|
removeReport(),
|
|
4942
|
-
|
|
5902
|
+
rm5(artifacts.artifactManifestJson, { force: true })
|
|
4943
5903
|
]);
|
|
4944
5904
|
return;
|
|
4945
5905
|
}
|
|
@@ -4953,9 +5913,9 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
4953
5913
|
}
|
|
4954
5914
|
if (stage === "train") {
|
|
4955
5915
|
await Promise.all([
|
|
4956
|
-
|
|
5916
|
+
rm5(artifacts.trainingDir, { recursive: true, force: true }),
|
|
4957
5917
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
4958
|
-
|
|
5918
|
+
rm5(resolve14(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
4959
5919
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
4960
5920
|
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
4961
5921
|
removeReport()
|
|
@@ -4974,7 +5934,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
4974
5934
|
await removeReport();
|
|
4975
5935
|
}
|
|
4976
5936
|
function manifestRelativePath(artifacts, path) {
|
|
4977
|
-
return
|
|
5937
|
+
return relative5(artifacts.runDir, path).split("\\").join("/");
|
|
4978
5938
|
}
|
|
4979
5939
|
async function verifyReusableArtifacts(artifacts, paths, options = {}) {
|
|
4980
5940
|
if (!await pathExists2(artifacts.artifactManifestJson)) return false;
|
|
@@ -4991,11 +5951,11 @@ async function throwIfCancelled(store, request) {
|
|
|
4991
5951
|
}
|
|
4992
5952
|
}
|
|
4993
5953
|
async function acquireWorkflowLock(lockPath, description) {
|
|
4994
|
-
await
|
|
4995
|
-
const token =
|
|
5954
|
+
await mkdir11(dirname9(lockPath), { recursive: true });
|
|
5955
|
+
const token = randomUUID6();
|
|
4996
5956
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
4997
5957
|
try {
|
|
4998
|
-
const handle = await
|
|
5958
|
+
const handle = await open4(lockPath, "wx", 384);
|
|
4999
5959
|
try {
|
|
5000
5960
|
await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
5001
5961
|
`);
|
|
@@ -5004,17 +5964,17 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
5004
5964
|
}
|
|
5005
5965
|
return async () => {
|
|
5006
5966
|
try {
|
|
5007
|
-
const owner = JSON.parse(await
|
|
5008
|
-
if (owner.token === token) await
|
|
5967
|
+
const owner = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
5968
|
+
if (owner.token === token) await rm5(lockPath, { force: true });
|
|
5009
5969
|
} catch {
|
|
5010
5970
|
}
|
|
5011
5971
|
};
|
|
5012
5972
|
} catch (error) {
|
|
5013
5973
|
if (error.code !== "EEXIST") throw error;
|
|
5014
|
-
const metadata = await
|
|
5974
|
+
const metadata = await stat9(lockPath).catch(() => null);
|
|
5015
5975
|
let owner = null;
|
|
5016
5976
|
try {
|
|
5017
|
-
owner = JSON.parse(await
|
|
5977
|
+
owner = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
5018
5978
|
} catch {
|
|
5019
5979
|
owner = null;
|
|
5020
5980
|
}
|
|
@@ -5033,10 +5993,10 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
5033
5993
|
`${description} already has an active local workflow (lock: ${lockPath}).`
|
|
5034
5994
|
);
|
|
5035
5995
|
}
|
|
5036
|
-
const stalePath = `${lockPath}.stale.${
|
|
5996
|
+
const stalePath = `${lockPath}.stale.${randomUUID6()}`;
|
|
5037
5997
|
try {
|
|
5038
|
-
await
|
|
5039
|
-
await
|
|
5998
|
+
await rename4(lockPath, stalePath);
|
|
5999
|
+
await rm5(stalePath, { force: true });
|
|
5040
6000
|
} catch (renameError) {
|
|
5041
6001
|
if (renameError.code !== "ENOENT") throw renameError;
|
|
5042
6002
|
}
|
|
@@ -5046,11 +6006,11 @@ async function acquireWorkflowLock(lockPath, description) {
|
|
|
5046
6006
|
}
|
|
5047
6007
|
async function acquireRunLock(store, runId) {
|
|
5048
6008
|
await store.ensure();
|
|
5049
|
-
return acquireWorkflowLock(
|
|
6009
|
+
return acquireWorkflowLock(resolve14(store.paths.runsDir, runId, "workflow.lock"), `Run ${runId}`);
|
|
5050
6010
|
}
|
|
5051
6011
|
async function acquireArtifactLock(artifacts) {
|
|
5052
6012
|
return acquireWorkflowLock(
|
|
5053
|
-
|
|
6013
|
+
join12(artifacts.runDir, ARTIFACT_WORKFLOW_LOCK_FILE),
|
|
5054
6014
|
`Artifact directory ${artifacts.runDir}`
|
|
5055
6015
|
);
|
|
5056
6016
|
}
|
|
@@ -5097,13 +6057,13 @@ function stageFingerprintPath(prepared, stage) {
|
|
|
5097
6057
|
return `${stageOutputPath(prepared, stage)}.stage.json`;
|
|
5098
6058
|
}
|
|
5099
6059
|
async function runnerFileFingerprints() {
|
|
5100
|
-
const project =
|
|
5101
|
-
const bundledSource =
|
|
5102
|
-
const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) =>
|
|
6060
|
+
const project = resolve14(packageRoot2, "training/adapter");
|
|
6061
|
+
const bundledSource = resolve14(packageRoot2, "training/adapter/src");
|
|
6062
|
+
const pythonSources = (await readdir5(bundledSource, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) => resolve14(bundledSource, entry.name));
|
|
5103
6063
|
const candidates = [.../* @__PURE__ */ new Set([
|
|
5104
6064
|
...pythonSources,
|
|
5105
|
-
|
|
5106
|
-
|
|
6065
|
+
resolve14(project, "pyproject.toml"),
|
|
6066
|
+
resolve14(project, "uv.lock")
|
|
5107
6067
|
])];
|
|
5108
6068
|
const fingerprints = {};
|
|
5109
6069
|
for (const path of candidates.sort()) fingerprints[path] = await hashFileIfPresent(path);
|
|
@@ -5217,7 +6177,7 @@ async function ensureRunRecord(args) {
|
|
|
5217
6177
|
`Run ${args.request.run_id} cannot be reused with different user, behavior spec, or run number identity.`
|
|
5218
6178
|
);
|
|
5219
6179
|
}
|
|
5220
|
-
if (
|
|
6180
|
+
if (resolve14(existing.artifact_dir) !== resolve14(args.artifacts.runDir)) {
|
|
5221
6181
|
throw new Error(
|
|
5222
6182
|
`Run ${args.request.run_id} is already bound to artifact directory ${existing.artifact_dir}; it cannot be resumed with ${args.artifacts.runDir}.`
|
|
5223
6183
|
);
|
|
@@ -5295,7 +6255,7 @@ async function computePreparedRun(args) {
|
|
|
5295
6255
|
base_model_for_evaluation: baseModelForEvaluation,
|
|
5296
6256
|
base_model_revision: baseModelRevision ?? null,
|
|
5297
6257
|
base_model_fingerprint: baseModelFingerprint ?? null,
|
|
5298
|
-
system_prompt_sha256:
|
|
6258
|
+
system_prompt_sha256: createHash9("sha256").update(system).digest("hex"),
|
|
5299
6259
|
prepared_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5300
6260
|
};
|
|
5301
6261
|
if (args.writeArtifacts) await writeJsonAtomic(artifacts.stageMetadataJson, metadata);
|
|
@@ -5324,7 +6284,7 @@ async function prepareStage(args) {
|
|
|
5324
6284
|
const preparedExists = await pathExists2(args.artifacts.stageMetadataJson) && await pathExists2(args.artifacts.trainingJsonl);
|
|
5325
6285
|
const prepared = await computePreparedRun({ ...args, writeArtifacts: false });
|
|
5326
6286
|
await args.store.syncRunRequest(args.request, args.artifacts.runDir);
|
|
5327
|
-
await writeJsonAtomic(
|
|
6287
|
+
await writeJsonAtomic(resolve14(args.artifacts.runDir, "request.json"), args.request);
|
|
5328
6288
|
const existingMetadata = preparedExists ? await readStageMetadata(args.artifacts.stageMetadataJson) : null;
|
|
5329
6289
|
let canReuse = preparedExists && existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
5330
6290
|
if (canReuse) {
|
|
@@ -5658,6 +6618,56 @@ async function runCandidateStage(args) {
|
|
|
5658
6618
|
});
|
|
5659
6619
|
return report;
|
|
5660
6620
|
}
|
|
6621
|
+
async function buildRunProvenance(args) {
|
|
6622
|
+
const { metadata, request } = args.prepared;
|
|
6623
|
+
const gpu = args.config.gpu;
|
|
6624
|
+
return runProvenanceSchema.parse({
|
|
6625
|
+
schema_version: 1,
|
|
6626
|
+
recorded_at: args.recordedAt,
|
|
6627
|
+
software: {
|
|
6628
|
+
tt_version: await packageVersion2(),
|
|
6629
|
+
node_version: process.version,
|
|
6630
|
+
platform: process.platform,
|
|
6631
|
+
arch: process.arch,
|
|
6632
|
+
runtime_fingerprint: metadata.runtime_fingerprint,
|
|
6633
|
+
python_lock_sha256: await hashFileIfPresent(resolve14(packageRoot2, "training/adapter/uv.lock"))
|
|
6634
|
+
},
|
|
6635
|
+
execution: {
|
|
6636
|
+
target: gpu ? "aws" : "local",
|
|
6637
|
+
training_backend: args.training.provider,
|
|
6638
|
+
...gpu ? { instance_id: gpu.instanceId, ...gpu.region ? { region: gpu.region } : {} } : {}
|
|
6639
|
+
},
|
|
6640
|
+
spec: {
|
|
6641
|
+
behavior_spec_id: request.behavior_spec_id,
|
|
6642
|
+
run_number: request.run_number,
|
|
6643
|
+
snapshot_sha256: hashJson(request.spec_snapshot),
|
|
6644
|
+
system_prompt_sha256: metadata.system_prompt_sha256,
|
|
6645
|
+
...args.specFile ? { file_path: args.specFile.path, file_sha256: args.specFile.sha256 } : {}
|
|
6646
|
+
},
|
|
6647
|
+
base_model: {
|
|
6648
|
+
id: request.spec_snapshot.base_model,
|
|
6649
|
+
revision: metadata.base_model_revision,
|
|
6650
|
+
fingerprint: metadata.base_model_fingerprint
|
|
6651
|
+
},
|
|
6652
|
+
data: {
|
|
6653
|
+
prebuilt: metadata.dataset_prebuilt,
|
|
6654
|
+
format: metadata.dataset_format,
|
|
6655
|
+
file_sha256: metadata.dataset_fingerprints,
|
|
6656
|
+
compiled_training_sha256: await hashFileIfPresent(args.prepared.artifacts.trainingJsonl),
|
|
6657
|
+
training_examples: metadata.training_example_count,
|
|
6658
|
+
eval_split: metadata.eval_split,
|
|
6659
|
+
eval_sample_seed: metadata.eval_sample_seed,
|
|
6660
|
+
eval_examples_total: metadata.eval_examples_total,
|
|
6661
|
+
eval_examples_used: metadata.eval_examples_used
|
|
6662
|
+
},
|
|
6663
|
+
training: {
|
|
6664
|
+
hyperparameters: request.hyperparameters,
|
|
6665
|
+
exit_code: args.training.exit_code,
|
|
6666
|
+
log_uri: args.training.log_uri
|
|
6667
|
+
},
|
|
6668
|
+
source_fingerprint: metadata.source_fingerprint
|
|
6669
|
+
});
|
|
6670
|
+
}
|
|
5661
6671
|
async function runReportStage(args) {
|
|
5662
6672
|
if (!await pathExists2(args.prepared.artifacts.baselineEvalJson)) {
|
|
5663
6673
|
throw new Error("Run reporting requires baseline-eval.json.");
|
|
@@ -5750,6 +6760,13 @@ async function runReportStage(args) {
|
|
|
5750
6760
|
}
|
|
5751
6761
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5752
6762
|
const duration = elapsed(args.startedPerf);
|
|
6763
|
+
const provenance = await buildRunProvenance({
|
|
6764
|
+
prepared: args.prepared,
|
|
6765
|
+
config: args.config,
|
|
6766
|
+
training,
|
|
6767
|
+
recordedAt: completedAt,
|
|
6768
|
+
specFile: args.specFile
|
|
6769
|
+
});
|
|
5753
6770
|
const report = runReportSchema.parse({
|
|
5754
6771
|
run_id: args.prepared.request.run_id,
|
|
5755
6772
|
behavior_spec_id: args.prepared.request.behavior_spec_id,
|
|
@@ -5763,6 +6780,7 @@ async function runReportStage(args) {
|
|
|
5763
6780
|
comparison,
|
|
5764
6781
|
general_regression: generalRegression,
|
|
5765
6782
|
training,
|
|
6783
|
+
provenance,
|
|
5766
6784
|
artifact_uris: {
|
|
5767
6785
|
dataset: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
5768
6786
|
baseline_eval: fileUri(args.prepared.artifacts.baselineEvalJson),
|
|
@@ -5777,6 +6795,7 @@ async function runReportStage(args) {
|
|
|
5777
6795
|
dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
|
|
5778
6796
|
dataset_format: args.prepared.metadata.dataset_format,
|
|
5779
6797
|
dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
6798
|
+
...args.prepared.request.dataset_source ? { dataset_source: args.prepared.request.dataset_source } : {},
|
|
5780
6799
|
spec_example_count: args.prepared.request.spec_snapshot.examples.length,
|
|
5781
6800
|
training_example_count: args.prepared.metadata.training_example_count,
|
|
5782
6801
|
eval_examples_total: baseline.eval_examples_total,
|
|
@@ -5849,7 +6868,7 @@ async function runLocalPipeline(input) {
|
|
|
5849
6868
|
store,
|
|
5850
6869
|
reporter: input.reporter
|
|
5851
6870
|
});
|
|
5852
|
-
await writeJsonAtomic(
|
|
6871
|
+
await writeJsonAtomic(join12(artifacts.runDir, "resolved-workflow.json"), {
|
|
5853
6872
|
spec: input.projectSpec ?? {
|
|
5854
6873
|
...input.request.spec_snapshot,
|
|
5855
6874
|
hyperparameters: input.request.hyperparameters,
|
|
@@ -5871,7 +6890,7 @@ async function runLocalPipeline(input) {
|
|
|
5871
6890
|
if (step.uses === "evaluate") {
|
|
5872
6891
|
const isBase = step.with.model === "base";
|
|
5873
6892
|
const report = isBase ? await runBaselineStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter }) : await runCandidateStage({ prepared, config: input.config, store, reporter: input.reporter, runReporter });
|
|
5874
|
-
const reportPath =
|
|
6893
|
+
const reportPath = join12(artifacts.runDir, "evaluations", `${step.id}.json`);
|
|
5875
6894
|
await writeJsonAtomic(reportPath, { ...report, artifact_uri: fileUri(reportPath) });
|
|
5876
6895
|
outputs[step.id] = { report: { ...report, artifact_uri: fileUri(reportPath) }, reportPath };
|
|
5877
6896
|
await refreshArtifactManifest(prepared);
|
|
@@ -5890,7 +6909,8 @@ async function runLocalPipeline(input) {
|
|
|
5890
6909
|
store,
|
|
5891
6910
|
reporter: input.reporter,
|
|
5892
6911
|
startedAt,
|
|
5893
|
-
startedPerf
|
|
6912
|
+
startedPerf,
|
|
6913
|
+
specFile: input.specFile
|
|
5894
6914
|
});
|
|
5895
6915
|
return { request: input.request, pipeline, status: "completed", outputs, report, reportPath: artifacts.runReportJson, artifactDir: artifacts.runDir };
|
|
5896
6916
|
}
|
|
@@ -5948,8 +6968,8 @@ async function runLocalFineTune(input) {
|
|
|
5948
6968
|
|
|
5949
6969
|
// src/local-runtime/project-workflow.ts
|
|
5950
6970
|
import { existsSync as existsSync3 } from "fs";
|
|
5951
|
-
import { readFile as
|
|
5952
|
-
import { dirname as dirname10, join as
|
|
6971
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6972
|
+
import { dirname as dirname10, join as join13, resolve as resolve15 } from "path";
|
|
5953
6973
|
function configForSpec(spec, specPath) {
|
|
5954
6974
|
return parseLocalRunnerConfig({
|
|
5955
6975
|
storeRoot: process.env.TT_LOCAL_HOME?.trim() ? expandUserPath(process.env.TT_LOCAL_HOME.trim()) : DEFAULT_PROJECT_STORE_ROOT,
|
|
@@ -5960,9 +6980,9 @@ function configForSpec(spec, specPath) {
|
|
|
5960
6980
|
async function resolveProjectConfig(specPath, configPath, warn, loaded) {
|
|
5961
6981
|
const input = loaded ?? (existsSync3(specPath) ? await loadLocalRunInput(specPath) : void 0);
|
|
5962
6982
|
const spec = input && input.kind !== "request" ? input.spec : void 0;
|
|
5963
|
-
const candidate = configPath ?
|
|
6983
|
+
const candidate = configPath ? resolve15(configPath) : join13(dirname10(resolve15(specPath)), "local-runner.json");
|
|
5964
6984
|
if (configPath || existsSync3(candidate)) {
|
|
5965
|
-
const legacy = parseLocalRunnerConfig(JSON.parse(await
|
|
6985
|
+
const legacy = parseLocalRunnerConfig(JSON.parse(await readFile11(candidate, "utf8")), candidate);
|
|
5966
6986
|
if (spec?.runtime || spec?.evaluation) {
|
|
5967
6987
|
throw new Error("Conflicting configuration sources: tunedtensor.json contains runtime/evaluation settings and a legacy runner config is present. Migrate to tunedtensor.json; do not silently override the spec.");
|
|
5968
6988
|
}
|
|
@@ -6092,6 +7112,9 @@ function trainingGpu(inventory) {
|
|
|
6092
7112
|
if (!listed.memory_total_bytes && inventory.python?.total_memory_bytes) {
|
|
6093
7113
|
return { ...listed, memory_total_bytes: inventory.python.total_memory_bytes };
|
|
6094
7114
|
}
|
|
7115
|
+
if (!listed.memory_total_bytes && (listed.unified_memory || isUnifiedGpuName(listed.name))) {
|
|
7116
|
+
return { ...listed, memory_total_bytes: inventory.os.total_memory_bytes, unified_memory: true };
|
|
7117
|
+
}
|
|
6095
7118
|
return listed;
|
|
6096
7119
|
}
|
|
6097
7120
|
if (inventory.python?.cuda_device) {
|
|
@@ -6275,7 +7298,7 @@ function warningsFromSnapshot(report, target) {
|
|
|
6275
7298
|
import { constants as constants2 } from "fs";
|
|
6276
7299
|
import { access as access2, statfs } from "fs/promises";
|
|
6277
7300
|
import { cpus, freemem, homedir as homedir5, totalmem, type } from "os";
|
|
6278
|
-
import { dirname as dirname11, join as
|
|
7301
|
+
import { dirname as dirname11, join as join14, resolve as resolve16 } from "path";
|
|
6279
7302
|
import { spawn as spawn2 } from "child_process";
|
|
6280
7303
|
async function runCommand(command, args, options = {}) {
|
|
6281
7304
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
@@ -6429,7 +7452,7 @@ async function queryNvidiaGpus(env) {
|
|
|
6429
7452
|
return { result: table.code === 0 || !last ? table : last, gpus };
|
|
6430
7453
|
}
|
|
6431
7454
|
async function diskCheck(name, path) {
|
|
6432
|
-
const resolvedPath =
|
|
7455
|
+
const resolvedPath = resolve16(path);
|
|
6433
7456
|
try {
|
|
6434
7457
|
await access2(resolvedPath, constants2.R_OK);
|
|
6435
7458
|
const fs = await statfs(resolvedPath);
|
|
@@ -6530,10 +7553,10 @@ async function collectHostInventory(options = {}) {
|
|
|
6530
7553
|
const cwd = options.cwd ?? process.cwd();
|
|
6531
7554
|
const nodeVersion = process.versions.node;
|
|
6532
7555
|
const nodeMajor = Number(nodeVersion.split(".")[0]);
|
|
6533
|
-
const artifactRoot =
|
|
6534
|
-
const storeRoot =
|
|
6535
|
-
const modelCache =
|
|
6536
|
-
config?.paths.modelCache ?? env.HF_HOME ??
|
|
7556
|
+
const artifactRoot = resolve16(cwd, config?.artifactRoot ?? join14(cwd, DEFAULT_ARTIFACT_ROOT));
|
|
7557
|
+
const storeRoot = resolve16(config?.storeRoot ?? defaultStoreRoot(env));
|
|
7558
|
+
const modelCache = resolve16(
|
|
7559
|
+
config?.paths.modelCache ?? env.HF_HOME ?? join14(homedir5(), ".cache", "huggingface")
|
|
6537
7560
|
);
|
|
6538
7561
|
const [artifactDisk, storeDisk, cacheDisk, uvVersion, nvidiaQueried] = await Promise.all([
|
|
6539
7562
|
diskCheck("artifact-root", artifactRoot),
|
|
@@ -6579,17 +7602,17 @@ async function collectHostInventory(options = {}) {
|
|
|
6579
7602
|
}
|
|
6580
7603
|
|
|
6581
7604
|
// src/local-runtime/hardware-snapshot.ts
|
|
6582
|
-
import { chmod as chmod2, mkdir as
|
|
6583
|
-
import { dirname as dirname12, join as
|
|
7605
|
+
import { chmod as chmod2, mkdir as mkdir12, readFile as readFile12, writeFile as writeFile8 } from "fs/promises";
|
|
7606
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
6584
7607
|
var HARDWARE_SNAPSHOT_VERSION = 1;
|
|
6585
7608
|
var HARDWARE_SNAPSHOT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6586
7609
|
function hardwareSnapshotPath(env = process.env) {
|
|
6587
|
-
return
|
|
7610
|
+
return join15(getTunedTensorHome(env), "hardware.json");
|
|
6588
7611
|
}
|
|
6589
7612
|
async function readHardwareSnapshot(env = process.env) {
|
|
6590
7613
|
const path = hardwareSnapshotPath(env);
|
|
6591
7614
|
try {
|
|
6592
|
-
const parsed = JSON.parse(await
|
|
7615
|
+
const parsed = JSON.parse(await readFile12(path, "utf8"));
|
|
6593
7616
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
6594
7617
|
const snapshot = parsed;
|
|
6595
7618
|
if (snapshot.version !== HARDWARE_SNAPSHOT_VERSION) return void 0;
|
|
@@ -6606,7 +7629,7 @@ async function readHardwareSnapshot(env = process.env) {
|
|
|
6606
7629
|
}
|
|
6607
7630
|
async function writeHardwareSnapshot(snapshot, env = process.env) {
|
|
6608
7631
|
const path = hardwareSnapshotPath(env);
|
|
6609
|
-
await
|
|
7632
|
+
await mkdir12(dirname12(path), { recursive: true, mode: 448 });
|
|
6610
7633
|
const gpu = snapshot.capabilities.gpu;
|
|
6611
7634
|
const record = {
|
|
6612
7635
|
version: HARDWARE_SNAPSHOT_VERSION,
|
|
@@ -6617,7 +7640,7 @@ async function writeHardwareSnapshot(snapshot, env = process.env) {
|
|
|
6617
7640
|
capabilities: snapshot.capabilities,
|
|
6618
7641
|
summary: snapshot.summary ?? formatCapabilitySummary(snapshot.capabilities)
|
|
6619
7642
|
};
|
|
6620
|
-
await
|
|
7643
|
+
await writeFile8(path, `${JSON.stringify(record, null, 2)}
|
|
6621
7644
|
`, { mode: 384 });
|
|
6622
7645
|
await chmod2(path, 384);
|
|
6623
7646
|
return record;
|
|
@@ -6676,6 +7699,59 @@ async function assessHardware(options = {}) {
|
|
|
6676
7699
|
}, env);
|
|
6677
7700
|
}
|
|
6678
7701
|
|
|
7702
|
+
// src/local-runtime/console-reporter.ts
|
|
7703
|
+
function shortValue(value) {
|
|
7704
|
+
if (value === void 0 || value === null) return null;
|
|
7705
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
7706
|
+
return String(value);
|
|
7707
|
+
}
|
|
7708
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
7709
|
+
return null;
|
|
7710
|
+
}
|
|
7711
|
+
function formatRunEvent(event) {
|
|
7712
|
+
const detailText = Object.entries(event.details ?? {}).filter(([key]) => key !== "metrics").map(([key, value]) => {
|
|
7713
|
+
const formatted = key === "command" && Array.isArray(value) ? value.join(" ") : shortValue(value);
|
|
7714
|
+
return formatted ? `${key}=${formatted}` : null;
|
|
7715
|
+
}).filter((value) => Boolean(value)).slice(0, 5).join(" ");
|
|
7716
|
+
return sanitizeLogLine(`[tt] ${event.stage}: ${event.message}${detailText ? ` (${detailText})` : ""}`);
|
|
7717
|
+
}
|
|
7718
|
+
function elapsedLabel(milliseconds) {
|
|
7719
|
+
const total = Math.max(0, Math.floor(milliseconds / 1e3));
|
|
7720
|
+
const hours = Math.floor(total / 3600);
|
|
7721
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
7722
|
+
const seconds = total % 60;
|
|
7723
|
+
return `+${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
7724
|
+
}
|
|
7725
|
+
function createRunConsoleReporter(options = {}) {
|
|
7726
|
+
const write = options.write ?? ((text) => {
|
|
7727
|
+
process.stderr.write(text);
|
|
7728
|
+
});
|
|
7729
|
+
const now = options.now ?? (() => Date.now());
|
|
7730
|
+
const started = now();
|
|
7731
|
+
const stamp = (line) => line.replace(/^\[tt\] /, `[tt] ${elapsedLabel(now() - started)} `);
|
|
7732
|
+
const progress = createProgressRenderer({ write, tty: progressTty(), columns: progressColumns() });
|
|
7733
|
+
let lastLog = "";
|
|
7734
|
+
return {
|
|
7735
|
+
verbose: options.verbose ?? false,
|
|
7736
|
+
onProgress(update) {
|
|
7737
|
+
progress.update(update);
|
|
7738
|
+
},
|
|
7739
|
+
onEvent(event) {
|
|
7740
|
+
progress.interrupt();
|
|
7741
|
+
write(`${stamp(formatRunEvent(event))}
|
|
7742
|
+
`);
|
|
7743
|
+
},
|
|
7744
|
+
onLog(log) {
|
|
7745
|
+
const line = sanitizeLogLine(`[tt] ${log.stage}${log.stream ? ` ${log.stream}` : ""}: ${log.message}`);
|
|
7746
|
+
if (line === lastLog) return;
|
|
7747
|
+
lastLog = line;
|
|
7748
|
+
progress.interrupt();
|
|
7749
|
+
write(`${stamp(line)}
|
|
7750
|
+
`);
|
|
7751
|
+
}
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
|
|
6679
7755
|
export {
|
|
6680
7756
|
fileUri,
|
|
6681
7757
|
assertArtifactManifest,
|
|
@@ -6702,7 +7778,12 @@ export {
|
|
|
6702
7778
|
behaviorSpecSchema,
|
|
6703
7779
|
specSnapshotSchema,
|
|
6704
7780
|
foundationHyperparametersSchema,
|
|
7781
|
+
huggingFaceRepoIdSchema,
|
|
7782
|
+
datasetColumnsSchema,
|
|
7783
|
+
huggingFaceDatasetSourceSchema,
|
|
7784
|
+
isRepoRelativePath,
|
|
6705
7785
|
datasetPrebuiltSchema,
|
|
7786
|
+
datasetSourceSchema,
|
|
6706
7787
|
fineTuneHyperparametersSchema,
|
|
6707
7788
|
fineTuneRunRequestSchema,
|
|
6708
7789
|
evalExampleResultSchema,
|
|
@@ -6711,6 +7792,7 @@ export {
|
|
|
6711
7792
|
evalReportSchema,
|
|
6712
7793
|
comparisonReportSchema,
|
|
6713
7794
|
trainingReportSchema,
|
|
7795
|
+
runProvenanceSchema,
|
|
6714
7796
|
runReportSchema,
|
|
6715
7797
|
localRunnerConfigSchema,
|
|
6716
7798
|
projectRuntimeSchema,
|
|
@@ -6724,6 +7806,7 @@ export {
|
|
|
6724
7806
|
isFoundationSpecFile,
|
|
6725
7807
|
isDecisionSpecFile,
|
|
6726
7808
|
normalizeChatJsonlForRelocation,
|
|
7809
|
+
datasetInputIdentity,
|
|
6727
7810
|
buildSystemMessage,
|
|
6728
7811
|
exampleToChatRow,
|
|
6729
7812
|
compileSpecToJsonl,
|
|
@@ -6772,6 +7855,7 @@ export {
|
|
|
6772
7855
|
withOfflineHuggingFaceCacheEnvironment,
|
|
6773
7856
|
checkAwsGpu,
|
|
6774
7857
|
runGpuProcess,
|
|
7858
|
+
auditRun,
|
|
6775
7859
|
isTerminalRunState,
|
|
6776
7860
|
defaultLocalHome,
|
|
6777
7861
|
localStorePaths,
|
|
@@ -6784,8 +7868,13 @@ export {
|
|
|
6784
7868
|
compareEvalReports,
|
|
6785
7869
|
verifyModelPrefetchCacheReport,
|
|
6786
7870
|
buildModelPrefetchPayload,
|
|
7871
|
+
safeName,
|
|
6787
7872
|
verifyLocalBaseModel,
|
|
6788
7873
|
prefetchBaseModel,
|
|
7874
|
+
DatasetNotDownloadedError,
|
|
7875
|
+
huggingFaceDataset,
|
|
7876
|
+
prefetchHuggingFaceDataset,
|
|
7877
|
+
ensureDatasetCached,
|
|
6789
7878
|
evaluateGeneralRegressionGate,
|
|
6790
7879
|
canonicalLocalPipeline,
|
|
6791
7880
|
validateLocalPipeline,
|
|
@@ -6804,6 +7893,7 @@ export {
|
|
|
6804
7893
|
readHardwareSnapshot,
|
|
6805
7894
|
formatHostStatusLine,
|
|
6806
7895
|
formatAgentHostBlock,
|
|
6807
|
-
assessHardware
|
|
7896
|
+
assessHardware,
|
|
7897
|
+
createRunConsoleReporter
|
|
6808
7898
|
};
|
|
6809
|
-
//# sourceMappingURL=chunk-
|
|
7899
|
+
//# sourceMappingURL=chunk-4SUXBHIY.js.map
|