@tuned-tensor/local 0.2.6 → 0.2.8
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/CHANGELOG.md +60 -0
- package/README.md +149 -0
- package/dist/artifacts.d.ts +92 -0
- package/dist/artifacts.js +591 -3
- package/dist/artifacts.js.map +1 -1
- package/dist/contracts.d.ts +5 -2
- package/dist/contracts.js +15 -10
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +3 -0
- package/dist/dataset.js +87 -5
- package/dist/dataset.js.map +1 -1
- package/dist/doctor.d.ts +13 -2
- package/dist/doctor.js +372 -49
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +15 -0
- package/dist/evaluation.js +120 -16
- package/dist/evaluation.js.map +1 -1
- package/dist/huggingface-cache.d.ts +26 -0
- package/dist/huggingface-cache.js +68 -0
- package/dist/huggingface-cache.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +766 -73
- package/dist/index.js.map +1 -1
- package/dist/local-project.d.ts +11 -0
- package/dist/local-project.js +96 -3
- package/dist/local-project.js.map +1 -1
- package/dist/model-registry.d.ts +21 -0
- package/dist/model-registry.js +198 -0
- package/dist/model-registry.js.map +1 -1
- package/dist/model-server.d.ts +32 -0
- package/dist/model-server.js +158 -0
- package/dist/model-server.js.map +1 -0
- package/dist/orchestrator.d.ts +3 -0
- package/dist/orchestrator.js +1001 -142
- package/dist/orchestrator.js.map +1 -1
- package/dist/prefetch.d.ts +43 -0
- package/dist/prefetch.js +192 -0
- package/dist/prefetch.js.map +1 -0
- package/dist/process-runner.d.ts +7 -0
- package/dist/process-runner.js +171 -16
- package/dist/process-runner.js.map +1 -1
- package/dist/process-training.d.ts +5 -1
- package/dist/process-training.js +32 -16
- package/dist/process-training.js.map +1 -1
- package/dist/run-reporter.d.ts +2 -0
- package/dist/run-reporter.js +10 -0
- package/dist/run-reporter.js.map +1 -1
- package/dist/store.d.ts +16 -2
- package/dist/store.js +189 -24
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +32 -0
- package/docs/local-workflow-remediation-2026-07-13.md +130 -0
- package/docs/local-workflow-ux-review-2026-07-13.md +458 -0
- package/docs/spark.md +10 -0
- package/package.json +3 -1
- package/training/local-runner/pyproject.toml +1 -0
- package/training/local-runner/src/evaluate.py +80 -20
- package/training/local-runner/src/prefetch.py +178 -0
- package/training/local-runner/src/serve.py +329 -0
- package/training/local-runner/src/train.py +47 -22
- package/training/local-runner/src/train_dpo.py +41 -25
- package/training/local-runner/uv.lock +2401 -0
package/dist/orchestrator.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { copyFile, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { copyFile, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { performance } from "node:perf_hooks";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { defaultArtifactPrefix, fileUri, assertArtifactManifest, ARTIFACT_WORKFLOW_LOCK_FILE, claimRunArtifactDirectory, prepareRunDirectories, readJson, resolveRunArtifacts, writeArtifactManifest, writeFileAtomic, writeJsonAtomic, } from "./artifacts.js";
|
|
6
9
|
import { evalReportSchema, fineTuneRunRequestSchema, localRunnerConfigSchema, runReportSchema, trainingReportSchema, } from "./contracts.js";
|
|
7
|
-
import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec } from "./dataset.js";
|
|
10
|
+
import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec, localAssetPathsFromChatJsonl, normalizeChatJsonlForRelocation, } from "./dataset.js";
|
|
8
11
|
import { compareEvalReports, deriveSampleSeed, evaluateExamples, rescoreEvalReport, splitSpecExamples } from "./evaluation.js";
|
|
9
12
|
import { launchProcessTraining } from "./process-training.js";
|
|
13
|
+
import { assertUsableModelArtifact, localModelArtifactPath } from "./model-registry.js";
|
|
14
|
+
import { ProcessCancelledError } from "./process-runner.js";
|
|
10
15
|
import { createLocalStore } from "./store.js";
|
|
16
|
+
import { withHuggingFaceCacheEnvironment } from "./huggingface-cache.js";
|
|
17
|
+
import { verifyLocalBaseModel } from "./prefetch.js";
|
|
11
18
|
export async function loadJsonFile(path) {
|
|
12
19
|
return JSON.parse(await readFile(path, "utf8"));
|
|
13
20
|
}
|
|
@@ -17,7 +24,45 @@ export async function loadRunRequest(path) {
|
|
|
17
24
|
export async function loadLocalRunnerConfig(path) {
|
|
18
25
|
if (!path)
|
|
19
26
|
return localRunnerConfigSchema.parse({});
|
|
20
|
-
|
|
27
|
+
const configPath = resolve(path);
|
|
28
|
+
const base = dirname(configPath);
|
|
29
|
+
const config = localRunnerConfigSchema.parse(await loadJsonFile(configPath));
|
|
30
|
+
const configPathValue = (value, preserveBundled = false) => {
|
|
31
|
+
if (!value)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (preserveBundled && (value === "training/local-runner" || value.startsWith("training/local-runner/"))) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
if (value === "~")
|
|
37
|
+
return homedir();
|
|
38
|
+
if (value.startsWith("~/"))
|
|
39
|
+
return resolve(homedir(), value.slice(2));
|
|
40
|
+
return resolve(base, value);
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
artifactRoot: configPathValue(config.artifactRoot),
|
|
45
|
+
storeRoot: configPathValue(config.storeRoot),
|
|
46
|
+
training: {
|
|
47
|
+
...config.training,
|
|
48
|
+
cwd: configPathValue(config.training.cwd),
|
|
49
|
+
project: configPathValue(config.training.project, true),
|
|
50
|
+
script: configPathValue(config.training.script, true),
|
|
51
|
+
},
|
|
52
|
+
paths: {
|
|
53
|
+
baseModel: configPathValue(config.paths.baseModel),
|
|
54
|
+
modelCache: configPathValue(config.paths.modelCache),
|
|
55
|
+
},
|
|
56
|
+
evaluation: {
|
|
57
|
+
...config.evaluation,
|
|
58
|
+
inference: {
|
|
59
|
+
...config.evaluation.inference,
|
|
60
|
+
cwd: configPathValue(config.evaluation.inference.cwd),
|
|
61
|
+
project: configPathValue(config.evaluation.inference.project, true),
|
|
62
|
+
script: configPathValue(config.evaluation.inference.script, true),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
21
66
|
}
|
|
22
67
|
function elapsed(started) {
|
|
23
68
|
const ms = Math.max(0, Math.round(performance.now() - started));
|
|
@@ -60,19 +105,82 @@ function artifactPrefix(request) {
|
|
|
60
105
|
runId: request.run_id,
|
|
61
106
|
});
|
|
62
107
|
}
|
|
108
|
+
function canonicalJson(value) {
|
|
109
|
+
if (value === null || typeof value !== "object")
|
|
110
|
+
return JSON.stringify(value) ?? "null";
|
|
111
|
+
if (Array.isArray(value))
|
|
112
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
113
|
+
const object = value;
|
|
114
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
115
|
+
}
|
|
63
116
|
function hashJson(value) {
|
|
64
|
-
return createHash("sha256").update(
|
|
117
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
118
|
+
}
|
|
119
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
120
|
+
async function hashFileIfPresent(path) {
|
|
121
|
+
try {
|
|
122
|
+
return await hashFile(path);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function packageVersion() {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
131
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function effectiveConfigFingerprint(config) {
|
|
138
|
+
const bundledProject = resolve(packageRoot, "training/local-runner");
|
|
139
|
+
const projectRoots = [...new Set([
|
|
140
|
+
bundledProject,
|
|
141
|
+
config.training.project === "training/local-runner" ? bundledProject : config.training.project,
|
|
142
|
+
config.evaluation.inference.project === "training/local-runner"
|
|
143
|
+
? bundledProject
|
|
144
|
+
: config.evaluation.inference.project,
|
|
145
|
+
].filter((value) => Boolean(value)).map((value) => resolve(value)))];
|
|
146
|
+
const projectFingerprints = {};
|
|
147
|
+
for (const project of projectRoots) {
|
|
148
|
+
projectFingerprints[project] = {
|
|
149
|
+
pyproject: await hashFileIfPresent(resolve(project, "pyproject.toml")),
|
|
150
|
+
uv_lock: await hashFileIfPresent(resolve(project, "uv.lock")),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return hashJson({
|
|
154
|
+
config,
|
|
155
|
+
runtime: {
|
|
156
|
+
tt_local_version: await packageVersion(),
|
|
157
|
+
node_version: process.version,
|
|
158
|
+
platform: process.platform,
|
|
159
|
+
architecture: process.arch,
|
|
160
|
+
project_fingerprints: projectFingerprints,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function runtimeFingerprint() {
|
|
165
|
+
const bundledProject = resolve(packageRoot, "training/local-runner");
|
|
166
|
+
return hashJson({
|
|
167
|
+
tt_local_version: await packageVersion(),
|
|
168
|
+
node_version: process.version,
|
|
169
|
+
platform: process.platform,
|
|
170
|
+
architecture: process.arch,
|
|
171
|
+
runner_pyproject: await hashFileIfPresent(resolve(bundledProject, "pyproject.toml")),
|
|
172
|
+
runner_uv_lock: await hashFileIfPresent(resolve(bundledProject, "uv.lock")),
|
|
173
|
+
});
|
|
65
174
|
}
|
|
66
175
|
function preparedSourceFingerprint(args) {
|
|
67
176
|
return hashJson({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
177
|
+
request_fingerprint: args.requestFingerprint,
|
|
178
|
+
runtime_fingerprint: args.runtimeFingerprint,
|
|
179
|
+
preparation_config: args.preparationConfig,
|
|
180
|
+
base_model_revision: args.baseModelRevision ?? null,
|
|
181
|
+
base_model_fingerprint: args.baseModelFingerprint ?? null,
|
|
182
|
+
parent_model_fingerprint: args.parentModelFingerprint ?? null,
|
|
71
183
|
dataset_fingerprints: args.datasetFingerprints,
|
|
72
|
-
base_model_for_evaluation: args.baseModelForEvaluation,
|
|
73
|
-
parent_model_artifact: args.parentModelArtifact ?? null,
|
|
74
|
-
eval_sample_seed: args.evalSampleSeed,
|
|
75
|
-
max_eval_examples: args.maxEvalExamples ?? null,
|
|
76
184
|
});
|
|
77
185
|
}
|
|
78
186
|
async function countJsonlRows(path) {
|
|
@@ -80,24 +188,140 @@ async function countJsonlRows(path) {
|
|
|
80
188
|
return text.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
81
189
|
}
|
|
82
190
|
async function hashFile(path) {
|
|
83
|
-
|
|
191
|
+
const hash = createHash("sha256");
|
|
192
|
+
for await (const chunk of createReadStream(path))
|
|
193
|
+
hash.update(chunk);
|
|
194
|
+
return hash.digest("hex");
|
|
84
195
|
}
|
|
85
196
|
async function datasetFingerprints(request) {
|
|
86
197
|
const dataset = request.dataset_prebuilt;
|
|
87
|
-
if (!dataset)
|
|
88
|
-
return {};
|
|
89
198
|
const entries = [
|
|
90
|
-
["training", dataset
|
|
91
|
-
["validation", dataset
|
|
92
|
-
["test", dataset
|
|
199
|
+
["training", dataset?.training],
|
|
200
|
+
["validation", dataset?.validation],
|
|
201
|
+
["test", dataset?.test],
|
|
93
202
|
];
|
|
94
203
|
const fingerprints = {};
|
|
95
204
|
for (const [key, value] of entries) {
|
|
96
|
-
if (value)
|
|
97
|
-
|
|
205
|
+
if (!value)
|
|
206
|
+
continue;
|
|
207
|
+
const datasetPath = stripFileUri(value);
|
|
208
|
+
fingerprints[key] = await hashFile(datasetPath);
|
|
209
|
+
for (const assetPath of await localAssetPathsFromChatJsonl(datasetPath)) {
|
|
210
|
+
fingerprints[`${key}_asset:${assetPath}`] = await hashFile(assetPath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [exampleIndex, example] of request.spec_snapshot.examples.entries()) {
|
|
214
|
+
for (const [assetIndex, asset] of (example.input_assets ?? []).entries()) {
|
|
215
|
+
const value = asset.image ?? asset.data_uri ?? asset.uri ?? asset.path;
|
|
216
|
+
if (!value
|
|
217
|
+
|| value.startsWith("data:")
|
|
218
|
+
|| (/^[a-z][a-z0-9+.-]*:/i.test(value) && !value.startsWith("file://"))) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const assetPath = stripFileUri(value);
|
|
222
|
+
fingerprints[`spec_asset:${exampleIndex}:${assetIndex}:${assetPath}`] = await hashFile(assetPath);
|
|
223
|
+
}
|
|
98
224
|
}
|
|
99
225
|
return fingerprints;
|
|
100
226
|
}
|
|
227
|
+
async function resolveBaseModelRevision(request, config) {
|
|
228
|
+
const explicit = request.hyperparameters.base_model_revision;
|
|
229
|
+
if (explicit)
|
|
230
|
+
return explicit;
|
|
231
|
+
if (config.paths.baseModel) {
|
|
232
|
+
const match = resolve(config.paths.baseModel).match(/[\\/]snapshots[\\/]([^\\/]+)(?:[\\/]|$)/);
|
|
233
|
+
if (match?.[1])
|
|
234
|
+
return match[1];
|
|
235
|
+
}
|
|
236
|
+
if (!request.spec_snapshot.base_model.includes("/"))
|
|
237
|
+
return undefined;
|
|
238
|
+
const repository = `models--${request.spec_snapshot.base_model.replaceAll("/", "--")}`;
|
|
239
|
+
const cacheEnvironment = withHuggingFaceCacheEnvironment(process.env, config.paths.modelCache);
|
|
240
|
+
const refPath = resolve(cacheEnvironment.HF_HUB_CACHE, repository, "refs", "main");
|
|
241
|
+
try {
|
|
242
|
+
const revision = (await readFile(refPath, "utf8")).trim();
|
|
243
|
+
return revision || undefined;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function fingerprintLocalArtifact(uri) {
|
|
250
|
+
const root = localModelArtifactPath(uri);
|
|
251
|
+
const metadata = await lstat(root);
|
|
252
|
+
if (metadata.isSymbolicLink()) {
|
|
253
|
+
throw new Error(`Parent model artifact must not be a symbolic link: ${root}`);
|
|
254
|
+
}
|
|
255
|
+
if (metadata.isFile()) {
|
|
256
|
+
return hashJson({ kind: "file", size_bytes: metadata.size, sha256: await hashFile(root) });
|
|
257
|
+
}
|
|
258
|
+
if (!metadata.isDirectory())
|
|
259
|
+
throw new Error(`Parent model artifact is not a file or directory: ${root}`);
|
|
260
|
+
const files = [];
|
|
261
|
+
const visit = async (directory) => {
|
|
262
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
263
|
+
const child = resolve(directory, entry.name);
|
|
264
|
+
if (entry.isSymbolicLink()) {
|
|
265
|
+
throw new Error(`Parent model artifact must not contain symbolic links: ${child}`);
|
|
266
|
+
}
|
|
267
|
+
if (entry.isDirectory()) {
|
|
268
|
+
await visit(child);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const childMetadata = await stat(child);
|
|
272
|
+
if (!childMetadata.isFile())
|
|
273
|
+
continue;
|
|
274
|
+
files.push({
|
|
275
|
+
path: relative(root, child).split("\\").join("/"),
|
|
276
|
+
size_bytes: childMetadata.size,
|
|
277
|
+
sha256: await hashFile(child),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
await visit(root);
|
|
282
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
283
|
+
if (files.length === 0)
|
|
284
|
+
throw new Error(`Parent model artifact directory is empty: ${root}`);
|
|
285
|
+
return hashJson({ kind: "directory", files });
|
|
286
|
+
}
|
|
287
|
+
/** Hash a local base model while permitting Hugging Face snapshot file links. */
|
|
288
|
+
export async function fingerprintLocalBaseModel(uri) {
|
|
289
|
+
const root = localModelArtifactPath(uri);
|
|
290
|
+
const rootMetadata = await lstat(root);
|
|
291
|
+
if (rootMetadata.isSymbolicLink()) {
|
|
292
|
+
throw new Error(`Local base model path must not itself be a symbolic link: ${root}`);
|
|
293
|
+
}
|
|
294
|
+
if (!rootMetadata.isDirectory()) {
|
|
295
|
+
throw new Error(`Local base model must be a Hugging Face snapshot directory: ${root}`);
|
|
296
|
+
}
|
|
297
|
+
await verifyLocalBaseModel(root);
|
|
298
|
+
const files = [];
|
|
299
|
+
const visit = async (directory) => {
|
|
300
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
301
|
+
const child = resolve(directory, entry.name);
|
|
302
|
+
if (entry.isDirectory()) {
|
|
303
|
+
await visit(child);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const childMetadata = await stat(child);
|
|
307
|
+
if (entry.isSymbolicLink() && !childMetadata.isFile()) {
|
|
308
|
+
throw new Error(`Local base model contains a non-file symbolic link: ${child}`);
|
|
309
|
+
}
|
|
310
|
+
if (!childMetadata.isFile())
|
|
311
|
+
continue;
|
|
312
|
+
files.push({
|
|
313
|
+
path: relative(root, child).split("\\").join("/"),
|
|
314
|
+
size_bytes: childMetadata.size,
|
|
315
|
+
sha256: await hashFile(child),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
await visit(root);
|
|
320
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
321
|
+
if (files.length === 0)
|
|
322
|
+
throw new Error(`Local base model directory is empty: ${root}`);
|
|
323
|
+
return hashJson({ kind: "directory", files });
|
|
324
|
+
}
|
|
101
325
|
function statusForProgressStage(stage) {
|
|
102
326
|
if (stage === "evaluating_baseline")
|
|
103
327
|
return "evaluating_baseline";
|
|
@@ -125,24 +349,338 @@ async function readStageMetadata(path) {
|
|
|
125
349
|
}
|
|
126
350
|
}
|
|
127
351
|
async function clearDependentStageArtifacts(artifacts) {
|
|
352
|
+
await cleanupStageArtifacts(artifacts, "prepare");
|
|
353
|
+
}
|
|
354
|
+
async function removePrefixedArtifacts(path) {
|
|
355
|
+
const directory = dirname(path);
|
|
356
|
+
const prefix = `${basename(path)}.`;
|
|
357
|
+
const names = await readdir(directory).catch(() => []);
|
|
128
358
|
await Promise.all([
|
|
129
|
-
rm(
|
|
130
|
-
rm(
|
|
131
|
-
rm(artifacts.trainingReportJson, { force: true }),
|
|
132
|
-
rm(artifacts.runReportJson, { force: true }),
|
|
359
|
+
rm(path, { force: true }),
|
|
360
|
+
...names.filter((name) => name.startsWith(prefix)).map((name) => rm(resolve(directory, name), { recursive: true, force: true })),
|
|
133
361
|
]);
|
|
134
362
|
}
|
|
363
|
+
async function cleanupStageArtifacts(artifacts, stage) {
|
|
364
|
+
const removeReport = async () => rm(artifacts.runReportJson, { force: true });
|
|
365
|
+
if (stage === "prepare") {
|
|
366
|
+
await Promise.all([
|
|
367
|
+
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
368
|
+
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
369
|
+
rm(artifacts.trainingDir, { recursive: true, force: true }),
|
|
370
|
+
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
371
|
+
rm(resolve(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
372
|
+
removeReport(),
|
|
373
|
+
rm(artifacts.artifactManifestJson, { force: true }),
|
|
374
|
+
]);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (stage === "baseline") {
|
|
378
|
+
await Promise.all([
|
|
379
|
+
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
380
|
+
removeReport(),
|
|
381
|
+
]);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (stage === "train") {
|
|
385
|
+
await Promise.all([
|
|
386
|
+
rm(artifacts.trainingDir, { recursive: true, force: true }),
|
|
387
|
+
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
388
|
+
rm(resolve(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
389
|
+
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
390
|
+
removeReport(),
|
|
391
|
+
]);
|
|
392
|
+
await prepareRunDirectories(artifacts);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (stage === "candidate") {
|
|
396
|
+
await Promise.all([
|
|
397
|
+
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
398
|
+
removeReport(),
|
|
399
|
+
]);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await removeReport();
|
|
403
|
+
}
|
|
404
|
+
function manifestRelativePath(artifacts, path) {
|
|
405
|
+
return relative(artifacts.runDir, path).split("\\").join("/");
|
|
406
|
+
}
|
|
407
|
+
async function verifyReusableArtifacts(artifacts, paths, options = {}) {
|
|
408
|
+
if (!await pathExists(artifacts.artifactManifestJson))
|
|
409
|
+
return false;
|
|
410
|
+
await assertArtifactManifest(artifacts.artifactManifestJson, {
|
|
411
|
+
requiredPaths: paths.map((path) => manifestRelativePath(artifacts, path)),
|
|
412
|
+
scopeToRequired: true,
|
|
413
|
+
verifyModel: options.verifyModel,
|
|
414
|
+
});
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
async function throwIfCancelled(store, request) {
|
|
418
|
+
if (await store.isCancellationRequested(request.run_id)) {
|
|
419
|
+
throw new ProcessCancelledError(`Run ${request.run_id} was cancelled.`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function acquireWorkflowLock(lockPath, description) {
|
|
423
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
424
|
+
const token = randomUUID();
|
|
425
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
426
|
+
try {
|
|
427
|
+
const handle = await open(lockPath, "wx", 0o600);
|
|
428
|
+
try {
|
|
429
|
+
await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, created_at: new Date().toISOString() })}\n`);
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
await handle.close();
|
|
433
|
+
}
|
|
434
|
+
return async () => {
|
|
435
|
+
try {
|
|
436
|
+
const owner = JSON.parse(await readFile(lockPath, "utf8"));
|
|
437
|
+
if (owner.token === token)
|
|
438
|
+
await rm(lockPath, { force: true });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// The lock may already have been removed during process shutdown.
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (error.code !== "EEXIST")
|
|
447
|
+
throw error;
|
|
448
|
+
const metadata = await stat(lockPath).catch(() => null);
|
|
449
|
+
let owner = null;
|
|
450
|
+
try {
|
|
451
|
+
owner = JSON.parse(await readFile(lockPath, "utf8"));
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
owner = null;
|
|
455
|
+
}
|
|
456
|
+
let ownerAlive = false;
|
|
457
|
+
if (typeof owner?.pid === "number" && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
|
458
|
+
try {
|
|
459
|
+
process.kill(owner.pid, 0);
|
|
460
|
+
ownerAlive = true;
|
|
461
|
+
}
|
|
462
|
+
catch (killError) {
|
|
463
|
+
ownerAlive = killError.code !== "ESRCH";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const recent = metadata ? Date.now() - metadata.mtimeMs < 10_000 : true;
|
|
467
|
+
if (ownerAlive || (!owner && recent)) {
|
|
468
|
+
throw new Error(`${description} already has an active local workflow (lock: ${lockPath}).`);
|
|
469
|
+
}
|
|
470
|
+
const stalePath = `${lockPath}.stale.${randomUUID()}`;
|
|
471
|
+
try {
|
|
472
|
+
await rename(lockPath, stalePath);
|
|
473
|
+
await rm(stalePath, { force: true });
|
|
474
|
+
}
|
|
475
|
+
catch (renameError) {
|
|
476
|
+
if (renameError.code !== "ENOENT")
|
|
477
|
+
throw renameError;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`Unable to acquire local workflow lock for ${description}.`);
|
|
482
|
+
}
|
|
483
|
+
async function acquireRunLock(store, runId) {
|
|
484
|
+
return acquireWorkflowLock(resolve(store.paths.runsDir, runId, "workflow.lock"), `Run ${runId}`);
|
|
485
|
+
}
|
|
486
|
+
async function acquireArtifactLock(artifacts) {
|
|
487
|
+
return acquireWorkflowLock(join(artifacts.runDir, ARTIFACT_WORKFLOW_LOCK_FILE), `Artifact directory ${artifacts.runDir}`);
|
|
488
|
+
}
|
|
489
|
+
function isDryTraining(training) {
|
|
490
|
+
return training.metrics?.dry_run === true;
|
|
491
|
+
}
|
|
492
|
+
function stringMetadata(value, key) {
|
|
493
|
+
const entry = value?.[key];
|
|
494
|
+
return typeof entry === "string" && entry.length > 0 ? entry : undefined;
|
|
495
|
+
}
|
|
496
|
+
async function modelManifestContract(prepared, training) {
|
|
497
|
+
if (isDryTraining(training) || !training.model_artifact_uri)
|
|
498
|
+
return undefined;
|
|
499
|
+
const metadata = training.artifact_metadata;
|
|
500
|
+
const explicitContract = Boolean(metadata?.framework && metadata?.format);
|
|
501
|
+
const inspection = await assertUsableModelArtifact(training.model_artifact_uri, {
|
|
502
|
+
allowUnrecognizedPayload: explicitContract,
|
|
503
|
+
});
|
|
504
|
+
const adapterWeights = inspection.adapter_weight_file_count > 0
|
|
505
|
+
&& inspection.adapter_weight_bytes > 0;
|
|
506
|
+
const fullModelWeights = inspection.full_model_weight_file_count > 0
|
|
507
|
+
&& inspection.full_model_weight_bytes > 0;
|
|
508
|
+
if (!adapterWeights && !fullModelWeights && !explicitContract) {
|
|
509
|
+
throw new Error(`Model artifact ${inspection.path} has no loadable adapter or Transformers full-model weights. `
|
|
510
|
+
+ "Custom trainers must set training.artifact.framework and training.artifact.format.");
|
|
511
|
+
}
|
|
512
|
+
if ((metadata?.framework === "transformers-peft" || metadata?.servable === true)
|
|
513
|
+
&& (!adapterWeights || !inspection.has_adapter_config)) {
|
|
514
|
+
throw new Error(`Model artifact ${inspection.path} requires adapter_model.safetensors or adapter_model.bin and a non-empty `
|
|
515
|
+
+ "adapter_config.json for a PEFT/servable contract.");
|
|
516
|
+
}
|
|
517
|
+
if (metadata?.framework === "transformers-full" && !fullModelWeights) {
|
|
518
|
+
throw new Error(`Model artifact ${inspection.path} has no Transformers full-model weights.`);
|
|
519
|
+
}
|
|
520
|
+
const implicitPeftAdapter = adapterWeights && inspection.has_adapter_config;
|
|
521
|
+
const format = metadata?.format
|
|
522
|
+
?? (inspection.kind === "file" && inspection.path.endsWith(".tar.gz")
|
|
523
|
+
? "tar.gz"
|
|
524
|
+
: inspection.kind === "directory" ? "huggingface-directory" : "file");
|
|
525
|
+
return {
|
|
526
|
+
artifact_kind: inspection.kind,
|
|
527
|
+
format,
|
|
528
|
+
framework: metadata?.framework ?? (implicitPeftAdapter ? "transformers-peft" : fullModelWeights ? "transformers-full" : "custom"),
|
|
529
|
+
base_model: prepared.request.spec_snapshot.base_model,
|
|
530
|
+
base_model_revision: stringMetadata(metadata, "base_model_revision")
|
|
531
|
+
?? stringMetadata(training.metrics, "base_model_revision")
|
|
532
|
+
?? prepared.metadata.base_model_revision
|
|
533
|
+
?? prepared.request.hyperparameters.base_model_revision,
|
|
534
|
+
base_model_artifact_uri: training.base_model_artifact_uri,
|
|
535
|
+
base_model_fingerprint: prepared.metadata.base_model_fingerprint ?? undefined,
|
|
536
|
+
parent_model_artifact: prepared.request.hyperparameters.parent_model_artifact,
|
|
537
|
+
artifact_uri: training.model_artifact_uri,
|
|
538
|
+
artifact_root: inspection.path,
|
|
539
|
+
servable: metadata?.servable ?? implicitPeftAdapter,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function refreshArtifactManifest(prepared) {
|
|
543
|
+
let model;
|
|
544
|
+
if (await pathExists(prepared.artifacts.trainingReportJson)) {
|
|
545
|
+
const training = trainingReportSchema.parse(await readJson(prepared.artifacts.trainingReportJson));
|
|
546
|
+
model = await modelManifestContract(prepared, training);
|
|
547
|
+
}
|
|
548
|
+
await writeArtifactManifest(prepared.artifacts, { model });
|
|
549
|
+
}
|
|
550
|
+
function stageOutputPath(prepared, stage) {
|
|
551
|
+
if (stage === "baseline")
|
|
552
|
+
return prepared.artifacts.baselineEvalJson;
|
|
553
|
+
if (stage === "train")
|
|
554
|
+
return prepared.artifacts.trainingReportJson;
|
|
555
|
+
return prepared.artifacts.candidateEvalJson;
|
|
556
|
+
}
|
|
557
|
+
function stageFingerprintPath(prepared, stage) {
|
|
558
|
+
return `${stageOutputPath(prepared, stage)}.stage.json`;
|
|
559
|
+
}
|
|
560
|
+
function configuredPath(path, cwd) {
|
|
561
|
+
if (path === "training/local-runner" || path.startsWith("training/local-runner/")) {
|
|
562
|
+
return resolve(packageRoot, path);
|
|
563
|
+
}
|
|
564
|
+
if (path.startsWith("file://"))
|
|
565
|
+
return stripFileUri(path);
|
|
566
|
+
if (isAbsolute(path))
|
|
567
|
+
return path;
|
|
568
|
+
const fileLike = path.startsWith("./")
|
|
569
|
+
|| path.startsWith("../")
|
|
570
|
+
|| path.includes("/")
|
|
571
|
+
|| path.includes("\\")
|
|
572
|
+
|| /\.(?:py|js|mjs|cjs|ts|tsx|sh|bash|zsh|pl|rb)$/i.test(path);
|
|
573
|
+
return fileLike ? resolve(cwd ?? process.cwd(), path) : null;
|
|
574
|
+
}
|
|
575
|
+
async function entrypointFileFingerprints(args) {
|
|
576
|
+
const candidates = new Set();
|
|
577
|
+
for (const value of args.values) {
|
|
578
|
+
if (!value)
|
|
579
|
+
continue;
|
|
580
|
+
const path = configuredPath(value, args.cwd);
|
|
581
|
+
if (path)
|
|
582
|
+
candidates.add(path);
|
|
583
|
+
}
|
|
584
|
+
if (args.project) {
|
|
585
|
+
const project = configuredPath(args.project, args.cwd) ?? resolve(args.cwd ?? process.cwd(), args.project);
|
|
586
|
+
candidates.add(resolve(project, "pyproject.toml"));
|
|
587
|
+
candidates.add(resolve(project, "uv.lock"));
|
|
588
|
+
}
|
|
589
|
+
const fingerprints = {};
|
|
590
|
+
for (const path of [...candidates].sort())
|
|
591
|
+
fingerprints[path] = await hashFileIfPresent(path);
|
|
592
|
+
return fingerprints;
|
|
593
|
+
}
|
|
594
|
+
async function stageFingerprint(args) {
|
|
595
|
+
const common = {
|
|
596
|
+
source_fingerprint: args.prepared.metadata.source_fingerprint,
|
|
597
|
+
runtime_fingerprint: args.prepared.metadata.runtime_fingerprint,
|
|
598
|
+
dry_run: args.config.dryRun,
|
|
599
|
+
};
|
|
600
|
+
if (args.stage === "train") {
|
|
601
|
+
const entrypointFiles = await entrypointFileFingerprints({
|
|
602
|
+
values: [
|
|
603
|
+
args.config.training.script
|
|
604
|
+
?? (args.prepared.request.training_method === "dpo"
|
|
605
|
+
? "training/local-runner/src/train_dpo.py"
|
|
606
|
+
: "training/local-runner/src/train.py"),
|
|
607
|
+
...(args.config.training.command ?? []),
|
|
608
|
+
],
|
|
609
|
+
cwd: args.config.training.cwd,
|
|
610
|
+
project: args.config.training.project,
|
|
611
|
+
});
|
|
612
|
+
return hashJson({
|
|
613
|
+
...common,
|
|
614
|
+
training: args.config.training,
|
|
615
|
+
paths: args.config.paths,
|
|
616
|
+
entrypoint_files: entrypointFiles,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const selectedCommand = args.stage === "baseline"
|
|
620
|
+
? args.config.evaluation.baselineCommand
|
|
621
|
+
: args.config.evaluation.candidateCommand;
|
|
622
|
+
const entrypointFiles = await entrypointFileFingerprints({
|
|
623
|
+
values: [
|
|
624
|
+
args.config.evaluation.inference.script,
|
|
625
|
+
...(args.config.evaluation.inference.command ?? []),
|
|
626
|
+
...(selectedCommand ?? []),
|
|
627
|
+
],
|
|
628
|
+
cwd: args.config.evaluation.inference.cwd,
|
|
629
|
+
project: args.config.evaluation.inference.project,
|
|
630
|
+
});
|
|
631
|
+
const evaluation = {
|
|
632
|
+
command: args.stage === "baseline"
|
|
633
|
+
? args.config.evaluation.baselineCommand
|
|
634
|
+
: args.config.evaluation.candidateCommand,
|
|
635
|
+
inference: args.config.evaluation.inference,
|
|
636
|
+
scoring: args.config.evaluation.scoring,
|
|
637
|
+
timeout_ms: args.config.evaluation.timeoutMs,
|
|
638
|
+
baseline_cache: args.config.evaluation.baselineCache,
|
|
639
|
+
llm: args.config.llm,
|
|
640
|
+
model_cache: args.config.paths.modelCache,
|
|
641
|
+
};
|
|
642
|
+
return hashJson({
|
|
643
|
+
...common,
|
|
644
|
+
evaluation,
|
|
645
|
+
entrypoint_files: entrypointFiles,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async function writeStageFingerprint(args) {
|
|
649
|
+
await writeJsonAtomic(stageFingerprintPath(args.prepared, args.stage), {
|
|
650
|
+
schema_version: 1,
|
|
651
|
+
stage: args.stage,
|
|
652
|
+
fingerprint: await stageFingerprint(args),
|
|
653
|
+
written_at: new Date().toISOString(),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async function hasCurrentStageFingerprint(args) {
|
|
657
|
+
try {
|
|
658
|
+
const record = await readJson(stageFingerprintPath(args.prepared, args.stage));
|
|
659
|
+
return record.fingerprint === await stageFingerprint(args);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function canReuseStageArtifact(args) {
|
|
666
|
+
const output = stageOutputPath(args.prepared, args.stage);
|
|
667
|
+
if (!await pathExists(output) || !await hasCurrentStageFingerprint(args))
|
|
668
|
+
return false;
|
|
669
|
+
return verifyReusableArtifacts(args.prepared.artifacts, [output, stageFingerprintPath(args.prepared, args.stage), ...(args.additionalPaths ?? [])], { verifyModel: args.verifyModel });
|
|
670
|
+
}
|
|
135
671
|
function createStoreReporter(input) {
|
|
136
672
|
return {
|
|
137
673
|
verbose: input.reporter?.verbose,
|
|
138
674
|
async onEvent(event) {
|
|
139
|
-
await input.store.updateRun({
|
|
675
|
+
const state = await input.store.updateRun({
|
|
140
676
|
runId: input.request.run_id,
|
|
141
677
|
status: statusForProgressStage(event.stage),
|
|
142
678
|
stage: event.stage,
|
|
143
679
|
message: event.message,
|
|
144
680
|
details: event.details,
|
|
145
681
|
});
|
|
682
|
+
if (state.status === "cancelled")
|
|
683
|
+
throw new ProcessCancelledError(`Run ${input.request.run_id} was cancelled.`);
|
|
146
684
|
await input.reporter?.onEvent?.(event);
|
|
147
685
|
},
|
|
148
686
|
async onLog(log) {
|
|
@@ -158,6 +696,8 @@ async function updateRun(input) {
|
|
|
158
696
|
message: input.message,
|
|
159
697
|
details: input.details,
|
|
160
698
|
});
|
|
699
|
+
if (state.status === "cancelled")
|
|
700
|
+
throw new ProcessCancelledError(`Run ${input.request.run_id} was cancelled.`);
|
|
161
701
|
await input.reporter?.onEvent?.({
|
|
162
702
|
stage: input.stage,
|
|
163
703
|
status: input.status,
|
|
@@ -168,11 +708,26 @@ async function updateRun(input) {
|
|
|
168
708
|
}
|
|
169
709
|
async function ensureRunRecord(args) {
|
|
170
710
|
await prepareRunDirectories(args.artifacts);
|
|
171
|
-
|
|
711
|
+
let existing = null;
|
|
172
712
|
try {
|
|
173
|
-
await args.store.getRun(args.request.run_id);
|
|
713
|
+
existing = await args.store.getRun(args.request.run_id);
|
|
174
714
|
}
|
|
175
|
-
catch {
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (!(error instanceof Error) || !error.message.startsWith("Run not found:"))
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
if (existing) {
|
|
720
|
+
if (existing.behavior_spec_id !== args.request.behavior_spec_id
|
|
721
|
+
|| existing.user_id !== args.request.user_id
|
|
722
|
+
|| existing.run_number !== args.request.run_number) {
|
|
723
|
+
throw new Error(`Run ${args.request.run_id} cannot be reused with different user, behavior spec, or run number identity.`);
|
|
724
|
+
}
|
|
725
|
+
if (resolve(existing.artifact_dir) !== resolve(args.artifacts.runDir)) {
|
|
726
|
+
throw new Error(`Run ${args.request.run_id} is already bound to artifact directory ${existing.artifact_dir}; `
|
|
727
|
+
+ `it cannot be resumed with ${args.artifacts.runDir}.`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
176
731
|
await args.store.startRun({ request: args.request, artifactDir: args.artifacts.runDir });
|
|
177
732
|
await args.reporter?.onEvent?.({
|
|
178
733
|
stage: "queued",
|
|
@@ -210,8 +765,10 @@ async function computePreparedRun(args) {
|
|
|
210
765
|
+ "overstate improvement. Provide dataset_prebuilt.test or dataset_prebuilt.validation, or set "
|
|
211
766
|
+ "evaluation.allowPrebuiltTrainingEval=true to evaluate on the training split anyway.");
|
|
212
767
|
}
|
|
213
|
-
if (args.writeArtifacts)
|
|
214
|
-
await
|
|
768
|
+
if (args.writeArtifacts) {
|
|
769
|
+
const normalizedTraining = await normalizeChatJsonlForRelocation(trainingPath);
|
|
770
|
+
await writeFile(artifacts.trainingJsonl, `${normalizedTraining}\n`, "utf8");
|
|
771
|
+
}
|
|
215
772
|
examples = await examplesFromChatJsonl(evaluation.path);
|
|
216
773
|
trainingExampleCount = null;
|
|
217
774
|
}
|
|
@@ -232,20 +789,40 @@ async function computePreparedRun(args) {
|
|
|
232
789
|
const system = buildSystemMessage(request.spec_snapshot);
|
|
233
790
|
const baseModelForEvaluation = config.paths.baseModel ?? request.spec_snapshot.base_model;
|
|
234
791
|
const parentModelArtifact = request.hyperparameters.parent_model_artifact;
|
|
792
|
+
const parentModelFingerprint = parentModelArtifact
|
|
793
|
+
? await fingerprintLocalArtifact(parentModelArtifact)
|
|
794
|
+
: undefined;
|
|
235
795
|
const maxEvalExamples = config.evaluation.maxExamples ?? request.hyperparameters.max_eval_examples;
|
|
236
796
|
const evalExamplesUsed = Math.min(maxEvalExamples ?? examples.length, examples.length);
|
|
237
797
|
const fingerprints = await datasetFingerprints(request);
|
|
798
|
+
const requestFingerprint = hashJson(request);
|
|
799
|
+
const effectiveConfigFingerprintValue = await effectiveConfigFingerprint(config);
|
|
800
|
+
const runtimeFingerprintValue = await runtimeFingerprint();
|
|
801
|
+
const baseModelRevision = await resolveBaseModelRevision(request, config);
|
|
802
|
+
const baseModelFingerprint = config.paths.baseModel
|
|
803
|
+
? await fingerprintLocalBaseModel(config.paths.baseModel)
|
|
804
|
+
: undefined;
|
|
238
805
|
const metadata = {
|
|
239
806
|
run_id: request.run_id,
|
|
240
807
|
behavior_spec_id: request.behavior_spec_id,
|
|
241
808
|
user_id: request.user_id,
|
|
242
809
|
training_method: request.training_method,
|
|
810
|
+
request_fingerprint: requestFingerprint,
|
|
811
|
+
effective_config_fingerprint: effectiveConfigFingerprintValue,
|
|
812
|
+
runtime_fingerprint: runtimeFingerprintValue,
|
|
243
813
|
source_fingerprint: preparedSourceFingerprint({
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
814
|
+
requestFingerprint,
|
|
815
|
+
runtimeFingerprint: runtimeFingerprintValue,
|
|
816
|
+
preparationConfig: {
|
|
817
|
+
dry_run: config.dryRun,
|
|
818
|
+
base_model_path: config.paths.baseModel,
|
|
819
|
+
max_eval_examples: config.evaluation.maxExamples,
|
|
820
|
+
eval_sample_seed: config.evaluation.sampleSeed,
|
|
821
|
+
allow_prebuilt_training_eval: config.evaluation.allowPrebuiltTrainingEval,
|
|
822
|
+
},
|
|
823
|
+
baseModelRevision,
|
|
824
|
+
baseModelFingerprint,
|
|
825
|
+
parentModelFingerprint,
|
|
249
826
|
datasetFingerprints: fingerprints,
|
|
250
827
|
}),
|
|
251
828
|
eval_split: evalSplit,
|
|
@@ -259,12 +836,15 @@ async function computePreparedRun(args) {
|
|
|
259
836
|
dataset_fingerprints: fingerprints,
|
|
260
837
|
dataset_uri: fileUri(artifacts.trainingJsonl),
|
|
261
838
|
base_model_for_evaluation: baseModelForEvaluation,
|
|
839
|
+
base_model_revision: baseModelRevision ?? null,
|
|
840
|
+
base_model_fingerprint: baseModelFingerprint ?? null,
|
|
262
841
|
parent_model_artifact: parentModelArtifact ?? null,
|
|
842
|
+
parent_model_fingerprint: parentModelFingerprint ?? null,
|
|
263
843
|
system_prompt_sha256: createHash("sha256").update(system).digest("hex"),
|
|
264
844
|
prepared_at: new Date().toISOString(),
|
|
265
845
|
};
|
|
266
846
|
if (args.writeArtifacts)
|
|
267
|
-
await
|
|
847
|
+
await writeJsonAtomic(artifacts.stageMetadataJson, metadata);
|
|
268
848
|
return {
|
|
269
849
|
request,
|
|
270
850
|
artifacts,
|
|
@@ -279,13 +859,22 @@ async function prepareStage(args) {
|
|
|
279
859
|
const preparedExists = await pathExists(args.artifacts.stageMetadataJson)
|
|
280
860
|
&& await pathExists(args.artifacts.trainingJsonl);
|
|
281
861
|
const prepared = await computePreparedRun({ ...args, writeArtifacts: false });
|
|
862
|
+
await args.store.syncRunRequest(args.request, args.artifacts.runDir);
|
|
863
|
+
await writeJsonAtomic(resolve(args.artifacts.runDir, "request.json"), args.request);
|
|
282
864
|
const existingMetadata = preparedExists
|
|
283
865
|
? await readStageMetadata(args.artifacts.stageMetadataJson)
|
|
284
866
|
: null;
|
|
285
|
-
|
|
867
|
+
let canReuse = preparedExists
|
|
286
868
|
&& !args.force
|
|
287
869
|
&& existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
288
870
|
if (canReuse) {
|
|
871
|
+
canReuse = await verifyReusableArtifacts(args.artifacts, [
|
|
872
|
+
args.artifacts.stageMetadataJson,
|
|
873
|
+
args.artifacts.trainingJsonl,
|
|
874
|
+
]);
|
|
875
|
+
}
|
|
876
|
+
if (canReuse) {
|
|
877
|
+
await throwIfCancelled(args.store, args.request);
|
|
289
878
|
await updateRun({
|
|
290
879
|
...args,
|
|
291
880
|
status: "preparing",
|
|
@@ -302,8 +891,12 @@ async function prepareStage(args) {
|
|
|
302
891
|
message: "Preparing local run artifacts.",
|
|
303
892
|
details: { artifact_dir: args.artifacts.runDir },
|
|
304
893
|
});
|
|
894
|
+
await throwIfCancelled(args.store, args.request);
|
|
895
|
+
await args.store.invalidateRunOutputs(args.request.run_id, { report: true, model: true });
|
|
305
896
|
await clearDependentStageArtifacts(args.artifacts);
|
|
306
|
-
|
|
897
|
+
const refreshed = await computePreparedRun({ ...args, writeArtifacts: true });
|
|
898
|
+
await refreshArtifactManifest(refreshed);
|
|
899
|
+
return refreshed;
|
|
307
900
|
}
|
|
308
901
|
async function ensurePrepared(args) {
|
|
309
902
|
return prepareStage({
|
|
@@ -316,7 +909,13 @@ async function ensurePrepared(args) {
|
|
|
316
909
|
});
|
|
317
910
|
}
|
|
318
911
|
async function runBaselineStage(args) {
|
|
319
|
-
if (!args.force
|
|
912
|
+
if (!args.force
|
|
913
|
+
&& await canReuseStageArtifact({
|
|
914
|
+
stage: "baseline",
|
|
915
|
+
prepared: args.prepared,
|
|
916
|
+
config: args.config,
|
|
917
|
+
})) {
|
|
918
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
320
919
|
await updateRun({
|
|
321
920
|
store: args.store,
|
|
322
921
|
reporter: args.reporter,
|
|
@@ -326,8 +925,13 @@ async function runBaselineStage(args) {
|
|
|
326
925
|
message: "Reusing existing baseline evaluation.",
|
|
327
926
|
details: { path: args.prepared.artifacts.baselineEvalJson },
|
|
328
927
|
});
|
|
329
|
-
|
|
928
|
+
const report = evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
|
|
929
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
930
|
+
return report;
|
|
330
931
|
}
|
|
932
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
933
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
934
|
+
await cleanupStageArtifacts(args.prepared.artifacts, "baseline");
|
|
331
935
|
await updateRun({
|
|
332
936
|
store: args.store,
|
|
333
937
|
reporter: args.reporter,
|
|
@@ -343,10 +947,14 @@ async function runBaselineStage(args) {
|
|
|
343
947
|
parent_model_artifact: args.prepared.metadata.parent_model_artifact,
|
|
344
948
|
},
|
|
345
949
|
});
|
|
346
|
-
|
|
950
|
+
const report = await evaluateExamples({
|
|
347
951
|
kind: "baseline",
|
|
348
952
|
modelId: args.prepared.metadata.parent_model_artifact ?? args.prepared.baseModelForEvaluation,
|
|
349
953
|
baseModelId: args.prepared.baseModelForEvaluation,
|
|
954
|
+
baseModelRevision: args.config.paths.baseModel
|
|
955
|
+
? undefined
|
|
956
|
+
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
957
|
+
sourceFingerprint: args.prepared.metadata.source_fingerprint,
|
|
350
958
|
adapterPath: args.prepared.metadata.parent_model_artifact ?? undefined,
|
|
351
959
|
examples: args.prepared.examples,
|
|
352
960
|
system: args.prepared.system,
|
|
@@ -356,10 +964,21 @@ async function runBaselineStage(args) {
|
|
|
356
964
|
maxExamples: args.prepared.maxEvalExamples,
|
|
357
965
|
evalSplit: args.prepared.metadata.eval_split,
|
|
358
966
|
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
967
|
+
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
359
968
|
});
|
|
969
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
970
|
+
await writeStageFingerprint({ stage: "baseline", prepared: args.prepared, config: args.config });
|
|
971
|
+
return report;
|
|
360
972
|
}
|
|
361
973
|
async function runTrainStage(args) {
|
|
362
|
-
if (!args.force
|
|
974
|
+
if (!args.force
|
|
975
|
+
&& await canReuseStageArtifact({
|
|
976
|
+
stage: "train",
|
|
977
|
+
prepared: args.prepared,
|
|
978
|
+
config: args.config,
|
|
979
|
+
verifyModel: true,
|
|
980
|
+
})) {
|
|
981
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
363
982
|
await updateRun({
|
|
364
983
|
store: args.store,
|
|
365
984
|
reporter: args.reporter,
|
|
@@ -369,8 +988,22 @@ async function runTrainStage(args) {
|
|
|
369
988
|
message: "Reusing existing training result.",
|
|
370
989
|
details: { path: args.prepared.artifacts.trainingReportJson },
|
|
371
990
|
});
|
|
372
|
-
|
|
991
|
+
const training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
992
|
+
if (!isDryTraining(training)) {
|
|
993
|
+
await modelManifestContract(args.prepared, training);
|
|
994
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
995
|
+
await args.store.registerModel({
|
|
996
|
+
request: args.prepared.request,
|
|
997
|
+
training,
|
|
998
|
+
artifactDir: args.prepared.artifacts.runDir,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1002
|
+
return training;
|
|
373
1003
|
}
|
|
1004
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1005
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true, model: true });
|
|
1006
|
+
await cleanupStageArtifacts(args.prepared.artifacts, "train");
|
|
374
1007
|
await updateRun({
|
|
375
1008
|
store: args.store,
|
|
376
1009
|
reporter: args.reporter,
|
|
@@ -384,16 +1017,37 @@ async function runTrainStage(args) {
|
|
|
384
1017
|
request: args.prepared.request,
|
|
385
1018
|
artifacts: args.prepared.artifacts,
|
|
386
1019
|
config: args.config,
|
|
1020
|
+
baseModelRevision: args.config.paths.baseModel
|
|
1021
|
+
? undefined
|
|
1022
|
+
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
387
1023
|
reporter: args.runReporter,
|
|
1024
|
+
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
388
1025
|
});
|
|
389
|
-
await
|
|
1026
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1027
|
+
if (!isDryTraining(training)) {
|
|
1028
|
+
if (!training.model_artifact_uri) {
|
|
1029
|
+
throw new Error("Training process completed without a model_artifact_uri.");
|
|
1030
|
+
}
|
|
1031
|
+
await modelManifestContract(args.prepared, training);
|
|
1032
|
+
}
|
|
1033
|
+
await writeJsonAtomic(args.prepared.artifacts.trainingReportJson, training);
|
|
1034
|
+
await writeStageFingerprint({ stage: "train", prepared: args.prepared, config: args.config });
|
|
1035
|
+
await refreshArtifactManifest(args.prepared);
|
|
1036
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1037
|
+
if (!isDryTraining(training)) {
|
|
1038
|
+
await args.store.registerModel({
|
|
1039
|
+
request: args.prepared.request,
|
|
1040
|
+
training,
|
|
1041
|
+
artifactDir: args.prepared.artifacts.runDir,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
390
1044
|
return training;
|
|
391
1045
|
}
|
|
392
1046
|
async function writeExternalTrainingReport(args) {
|
|
393
1047
|
const training = trainingReportSchema.parse({
|
|
394
1048
|
provider: args.config.training.backend === "command" ? "local-command" : "local-uv",
|
|
395
1049
|
training_job_name: `external-${args.prepared.request.run_id}`,
|
|
396
|
-
model_artifact_uri: args.modelArtifact,
|
|
1050
|
+
model_artifact_uri: fileUri(localModelArtifactPath(args.modelArtifact)),
|
|
397
1051
|
base_model_artifact_uri: args.config.paths.baseModel ? fileUri(args.config.paths.baseModel) : undefined,
|
|
398
1052
|
parent_model_artifact_uri: args.prepared.metadata.parent_model_artifact ?? undefined,
|
|
399
1053
|
artifact_metadata: {
|
|
@@ -404,11 +1058,36 @@ async function writeExternalTrainingReport(args) {
|
|
|
404
1058
|
exit_code: null,
|
|
405
1059
|
log_uri: fileUri(args.prepared.artifacts.trainingReportJson),
|
|
406
1060
|
});
|
|
407
|
-
await
|
|
1061
|
+
await writeJsonAtomic(args.prepared.artifacts.trainingReportJson, training);
|
|
408
1062
|
return training;
|
|
409
1063
|
}
|
|
410
1064
|
async function runCandidateStage(args) {
|
|
411
|
-
|
|
1065
|
+
let verifiedTraining;
|
|
1066
|
+
if (!args.modelArtifact && await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
1067
|
+
const trainingCurrent = await canReuseStageArtifact({
|
|
1068
|
+
stage: "train",
|
|
1069
|
+
prepared: args.prepared,
|
|
1070
|
+
config: args.config,
|
|
1071
|
+
verifyModel: true,
|
|
1072
|
+
});
|
|
1073
|
+
if (trainingCurrent) {
|
|
1074
|
+
verifiedTraining = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (!args.force
|
|
1078
|
+
&& !args.modelArtifact
|
|
1079
|
+
&& verifiedTraining
|
|
1080
|
+
&& await canReuseStageArtifact({
|
|
1081
|
+
stage: "candidate",
|
|
1082
|
+
prepared: args.prepared,
|
|
1083
|
+
config: args.config,
|
|
1084
|
+
verifyModel: true,
|
|
1085
|
+
additionalPaths: [
|
|
1086
|
+
args.prepared.artifacts.trainingReportJson,
|
|
1087
|
+
stageFingerprintPath(args.prepared, "train"),
|
|
1088
|
+
],
|
|
1089
|
+
})) {
|
|
1090
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
412
1091
|
await updateRun({
|
|
413
1092
|
store: args.store,
|
|
414
1093
|
reporter: args.reporter,
|
|
@@ -418,8 +1097,28 @@ async function runCandidateStage(args) {
|
|
|
418
1097
|
message: "Reusing existing candidate evaluation.",
|
|
419
1098
|
details: { path: args.prepared.artifacts.candidateEvalJson },
|
|
420
1099
|
});
|
|
421
|
-
|
|
1100
|
+
const training = verifiedTraining;
|
|
1101
|
+
if (!isDryTraining(training)) {
|
|
1102
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1103
|
+
await args.store.registerModel({
|
|
1104
|
+
request: args.prepared.request,
|
|
1105
|
+
training,
|
|
1106
|
+
artifactDir: args.prepared.artifacts.runDir,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
const report = evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
|
|
1110
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1111
|
+
return report;
|
|
1112
|
+
}
|
|
1113
|
+
if (!args.modelArtifact && !verifiedTraining) {
|
|
1114
|
+
throw new Error("candidate stage requires current verified training output. Run --stage train first.");
|
|
422
1115
|
}
|
|
1116
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1117
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, {
|
|
1118
|
+
report: true,
|
|
1119
|
+
model: Boolean(args.modelArtifact),
|
|
1120
|
+
});
|
|
1121
|
+
await cleanupStageArtifacts(args.prepared.artifacts, "candidate");
|
|
423
1122
|
let training;
|
|
424
1123
|
if (args.modelArtifact) {
|
|
425
1124
|
training = await writeExternalTrainingReport({
|
|
@@ -427,9 +1126,10 @@ async function runCandidateStage(args) {
|
|
|
427
1126
|
config: args.config,
|
|
428
1127
|
modelArtifact: args.modelArtifact,
|
|
429
1128
|
});
|
|
1129
|
+
await writeStageFingerprint({ stage: "train", prepared: args.prepared, config: args.config });
|
|
430
1130
|
}
|
|
431
|
-
else if (
|
|
432
|
-
training =
|
|
1131
|
+
else if (verifiedTraining) {
|
|
1132
|
+
training = verifiedTraining;
|
|
433
1133
|
}
|
|
434
1134
|
else {
|
|
435
1135
|
throw new Error("candidate stage requires training output or --model-artifact.");
|
|
@@ -437,6 +1137,16 @@ async function runCandidateStage(args) {
|
|
|
437
1137
|
const modelArtifact = training.model_artifact_uri;
|
|
438
1138
|
if (!modelArtifact)
|
|
439
1139
|
throw new Error("candidate stage requires a model_artifact_uri in training-report.json or --model-artifact.");
|
|
1140
|
+
if (!isDryTraining(training)) {
|
|
1141
|
+
await modelManifestContract(args.prepared, training);
|
|
1142
|
+
await refreshArtifactManifest(args.prepared);
|
|
1143
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1144
|
+
await args.store.registerModel({
|
|
1145
|
+
request: args.prepared.request,
|
|
1146
|
+
training,
|
|
1147
|
+
artifactDir: args.prepared.artifacts.runDir,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
440
1150
|
await updateRun({
|
|
441
1151
|
store: args.store,
|
|
442
1152
|
reporter: args.reporter,
|
|
@@ -446,10 +1156,13 @@ async function runCandidateStage(args) {
|
|
|
446
1156
|
message: "Running candidate evaluation.",
|
|
447
1157
|
details: { model_artifact_uri: modelArtifact },
|
|
448
1158
|
});
|
|
449
|
-
|
|
1159
|
+
const report = await evaluateExamples({
|
|
450
1160
|
kind: "candidate",
|
|
451
1161
|
modelId: modelArtifact,
|
|
452
1162
|
baseModelId: args.prepared.baseModelForEvaluation,
|
|
1163
|
+
baseModelRevision: args.config.paths.baseModel
|
|
1164
|
+
? undefined
|
|
1165
|
+
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
453
1166
|
adapterPath: modelArtifact,
|
|
454
1167
|
examples: args.prepared.examples,
|
|
455
1168
|
system: args.prepared.system,
|
|
@@ -459,7 +1172,16 @@ async function runCandidateStage(args) {
|
|
|
459
1172
|
maxExamples: args.prepared.maxEvalExamples,
|
|
460
1173
|
evalSplit: args.prepared.metadata.eval_split,
|
|
461
1174
|
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
1175
|
+
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
1176
|
+
});
|
|
1177
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1178
|
+
await writeStageFingerprint({
|
|
1179
|
+
stage: "candidate",
|
|
1180
|
+
prepared: args.prepared,
|
|
1181
|
+
config: args.config,
|
|
1182
|
+
training,
|
|
462
1183
|
});
|
|
1184
|
+
return report;
|
|
463
1185
|
}
|
|
464
1186
|
async function runScoreStage(args) {
|
|
465
1187
|
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
@@ -468,6 +1190,15 @@ async function runScoreStage(args) {
|
|
|
468
1190
|
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
469
1191
|
throw new Error("score stage requires candidate-eval.json. Run --stage candidate first.");
|
|
470
1192
|
}
|
|
1193
|
+
await verifyReusableArtifacts(args.prepared.artifacts, [
|
|
1194
|
+
args.prepared.artifacts.baselineEvalJson,
|
|
1195
|
+
stageFingerprintPath(args.prepared, "baseline"),
|
|
1196
|
+
args.prepared.artifacts.candidateEvalJson,
|
|
1197
|
+
stageFingerprintPath(args.prepared, "candidate"),
|
|
1198
|
+
]);
|
|
1199
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1200
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1201
|
+
await cleanupStageArtifacts(args.prepared.artifacts, "score");
|
|
471
1202
|
await updateRun({
|
|
472
1203
|
store: args.store,
|
|
473
1204
|
reporter: args.reporter,
|
|
@@ -477,21 +1208,47 @@ async function runScoreStage(args) {
|
|
|
477
1208
|
message: "Rescoring existing baseline and candidate outputs.",
|
|
478
1209
|
details: { scoring_mode: args.config.evaluation.scoring.mode },
|
|
479
1210
|
});
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1211
|
+
const rollbackPaths = [
|
|
1212
|
+
args.prepared.artifacts.baselineEvalJson,
|
|
1213
|
+
stageFingerprintPath(args.prepared, "baseline"),
|
|
1214
|
+
args.prepared.artifacts.candidateEvalJson,
|
|
1215
|
+
stageFingerprintPath(args.prepared, "candidate"),
|
|
1216
|
+
];
|
|
1217
|
+
const originals = new Map();
|
|
1218
|
+
for (const path of rollbackPaths)
|
|
1219
|
+
originals.set(path, await readFile(path));
|
|
1220
|
+
try {
|
|
1221
|
+
const baseline = await rescoreEvalReport({
|
|
1222
|
+
report: evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson)),
|
|
1223
|
+
config: args.config,
|
|
1224
|
+
outputPath: args.prepared.artifacts.baselineEvalJson,
|
|
1225
|
+
system: args.prepared.system,
|
|
1226
|
+
reporter: args.runReporter,
|
|
1227
|
+
});
|
|
1228
|
+
const candidate = await rescoreEvalReport({
|
|
1229
|
+
report: evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson)),
|
|
1230
|
+
config: args.config,
|
|
1231
|
+
outputPath: args.prepared.artifacts.candidateEvalJson,
|
|
1232
|
+
system: args.prepared.system,
|
|
1233
|
+
reporter: args.runReporter,
|
|
1234
|
+
});
|
|
1235
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1236
|
+
await writeStageFingerprint({ stage: "baseline", prepared: args.prepared, config: args.config });
|
|
1237
|
+
const training = await pathExists(args.prepared.artifacts.trainingReportJson)
|
|
1238
|
+
? trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson))
|
|
1239
|
+
: undefined;
|
|
1240
|
+
await writeStageFingerprint({
|
|
1241
|
+
stage: "candidate",
|
|
1242
|
+
prepared: args.prepared,
|
|
1243
|
+
config: args.config,
|
|
1244
|
+
training,
|
|
1245
|
+
});
|
|
1246
|
+
return { baseline, candidate };
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
await Promise.all([...originals].map(([path, contents]) => writeFileAtomic(path, contents)));
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
495
1252
|
}
|
|
496
1253
|
async function runReportStage(args) {
|
|
497
1254
|
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
@@ -503,6 +1260,37 @@ async function runReportStage(args) {
|
|
|
503
1260
|
if (!await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
504
1261
|
throw new Error("report stage requires training-report.json. Run --stage train first, or --stage candidate --model-artifact <path>.");
|
|
505
1262
|
}
|
|
1263
|
+
const currentTraining = await canReuseStageArtifact({
|
|
1264
|
+
stage: "train",
|
|
1265
|
+
prepared: args.prepared,
|
|
1266
|
+
config: args.config,
|
|
1267
|
+
verifyModel: true,
|
|
1268
|
+
});
|
|
1269
|
+
const currentBaseline = await canReuseStageArtifact({
|
|
1270
|
+
stage: "baseline",
|
|
1271
|
+
prepared: args.prepared,
|
|
1272
|
+
config: args.config,
|
|
1273
|
+
});
|
|
1274
|
+
const currentCandidate = await canReuseStageArtifact({
|
|
1275
|
+
stage: "candidate",
|
|
1276
|
+
prepared: args.prepared,
|
|
1277
|
+
config: args.config,
|
|
1278
|
+
verifyModel: true,
|
|
1279
|
+
});
|
|
1280
|
+
if (!currentTraining || !currentBaseline || !currentCandidate) {
|
|
1281
|
+
throw new Error("report stage inputs are stale for the current request/config. Re-run baseline, train, and candidate as needed.");
|
|
1282
|
+
}
|
|
1283
|
+
await verifyReusableArtifacts(args.prepared.artifacts, [
|
|
1284
|
+
args.prepared.artifacts.baselineEvalJson,
|
|
1285
|
+
stageFingerprintPath(args.prepared, "baseline"),
|
|
1286
|
+
args.prepared.artifacts.candidateEvalJson,
|
|
1287
|
+
stageFingerprintPath(args.prepared, "candidate"),
|
|
1288
|
+
args.prepared.artifacts.trainingReportJson,
|
|
1289
|
+
stageFingerprintPath(args.prepared, "train"),
|
|
1290
|
+
], { verifyModel: true });
|
|
1291
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1292
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1293
|
+
await cleanupStageArtifacts(args.prepared.artifacts, "report");
|
|
506
1294
|
if (args.emitReportingEvent) {
|
|
507
1295
|
await updateRun({
|
|
508
1296
|
store: args.store,
|
|
@@ -559,15 +1347,20 @@ async function runReportStage(args) {
|
|
|
559
1347
|
},
|
|
560
1348
|
created_at: completedAt,
|
|
561
1349
|
});
|
|
562
|
-
await
|
|
563
|
-
await
|
|
1350
|
+
await writeJsonAtomic(args.prepared.artifacts.runReportJson, report);
|
|
1351
|
+
await refreshArtifactManifest(args.prepared);
|
|
1352
|
+
await throwIfCancelled(args.store, args.prepared.request);
|
|
1353
|
+
const completedState = await args.store.completeRun(report, args.prepared.artifacts.runDir, args.prepared.artifacts.runReportJson);
|
|
1354
|
+
if (completedState.status === "cancelled") {
|
|
1355
|
+
throw new ProcessCancelledError(`Run ${args.prepared.request.run_id} was cancelled.`);
|
|
1356
|
+
}
|
|
564
1357
|
await args.reporter?.onEvent?.({
|
|
565
1358
|
stage: "completed",
|
|
566
1359
|
status: "completed",
|
|
567
1360
|
message: "Run completed successfully.",
|
|
568
1361
|
details: {
|
|
569
1362
|
report_path: args.prepared.artifacts.runReportJson,
|
|
570
|
-
model_id: `local-${args.prepared.request.run_id}
|
|
1363
|
+
...(!isDryTraining(training) ? { model_id: `local-${args.prepared.request.run_id}` } : {}),
|
|
571
1364
|
avg_score_delta: comparison.avg_score_delta,
|
|
572
1365
|
elapsed_seconds: duration.seconds,
|
|
573
1366
|
},
|
|
@@ -581,89 +1374,154 @@ export async function runLocalFineTuneStage(input) {
|
|
|
581
1374
|
const prefix = artifactPrefix(input.request);
|
|
582
1375
|
const artifacts = resolveRunArtifacts({ artifactRoot: input.config.artifactRoot, prefix });
|
|
583
1376
|
const store = createLocalStore(input.config.storeRoot);
|
|
584
|
-
|
|
585
|
-
|
|
1377
|
+
const releaseRunLock = await acquireRunLock(store, input.request.run_id);
|
|
1378
|
+
let releaseArtifactLock;
|
|
586
1379
|
try {
|
|
587
|
-
|
|
588
|
-
request: input.request,
|
|
589
|
-
config: input.config,
|
|
1380
|
+
await claimRunArtifactDirectory({
|
|
590
1381
|
artifacts,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1382
|
+
runId: input.request.run_id,
|
|
1383
|
+
userId: input.request.user_id,
|
|
1384
|
+
behaviorSpecId: input.request.behavior_spec_id,
|
|
594
1385
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await
|
|
601
|
-
|
|
1386
|
+
releaseArtifactLock = await acquireArtifactLock(artifacts);
|
|
1387
|
+
await ensureRunRecord({ request: input.request, artifacts, store, reporter: input.reporter });
|
|
1388
|
+
const runReporter = createStoreReporter({ request: input.request, store, reporter: input.reporter });
|
|
1389
|
+
let prepared;
|
|
1390
|
+
try {
|
|
1391
|
+
await throwIfCancelled(store, input.request);
|
|
1392
|
+
prepared = await ensurePrepared({
|
|
1393
|
+
request: input.request,
|
|
602
1394
|
config: input.config,
|
|
1395
|
+
artifacts,
|
|
603
1396
|
store,
|
|
604
1397
|
reporter: input.reporter,
|
|
605
|
-
|
|
606
|
-
force: Boolean(input.force),
|
|
1398
|
+
forcePrepare: Boolean(input.force && (stage === "prepare" || stage === "all")),
|
|
607
1399
|
});
|
|
608
|
-
|
|
1400
|
+
await throwIfCancelled(store, input.request);
|
|
1401
|
+
let report;
|
|
1402
|
+
const completeRequestedStage = async (completedStage) => {
|
|
1403
|
+
await throwIfCancelled(store, input.request);
|
|
1404
|
+
const message = `${completedStage} stage completed.`;
|
|
1405
|
+
const completedState = await store.updateRun({
|
|
1406
|
+
runId: input.request.run_id,
|
|
1407
|
+
status: "stage_completed",
|
|
1408
|
+
stage: `${completedStage}_completed`,
|
|
1409
|
+
message,
|
|
1410
|
+
details: { requested_stage: completedStage },
|
|
1411
|
+
});
|
|
1412
|
+
if (completedState.status === "cancelled") {
|
|
1413
|
+
throw new ProcessCancelledError(`Run ${input.request.run_id} was cancelled.`);
|
|
1414
|
+
}
|
|
1415
|
+
await input.reporter?.onEvent?.({
|
|
1416
|
+
stage: `${completedStage}_completed`,
|
|
1417
|
+
status: "completed",
|
|
1418
|
+
message,
|
|
1419
|
+
details: { requested_stage: completedStage },
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
if (stage === "prepare") {
|
|
1423
|
+
await completeRequestedStage(stage);
|
|
609
1424
|
return stageResult({ request: input.request, stage, artifacts });
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1425
|
+
}
|
|
1426
|
+
if (stage === "baseline" || stage === "all") {
|
|
1427
|
+
await runBaselineStage({
|
|
1428
|
+
prepared,
|
|
1429
|
+
config: input.config,
|
|
1430
|
+
store,
|
|
1431
|
+
reporter: input.reporter,
|
|
1432
|
+
runReporter,
|
|
1433
|
+
force: Boolean(input.force),
|
|
1434
|
+
});
|
|
1435
|
+
await refreshArtifactManifest(prepared);
|
|
1436
|
+
if (stage === "baseline") {
|
|
1437
|
+
await completeRequestedStage(stage);
|
|
1438
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (stage === "train" || stage === "all") {
|
|
1442
|
+
await runTrainStage({
|
|
1443
|
+
prepared,
|
|
1444
|
+
config: input.config,
|
|
1445
|
+
store,
|
|
1446
|
+
reporter: input.reporter,
|
|
1447
|
+
runReporter,
|
|
1448
|
+
force: Boolean(input.force),
|
|
1449
|
+
});
|
|
1450
|
+
if (stage === "train") {
|
|
1451
|
+
await completeRequestedStage(stage);
|
|
1452
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (stage === "candidate" || stage === "all") {
|
|
1456
|
+
await runCandidateStage({
|
|
1457
|
+
prepared,
|
|
1458
|
+
config: input.config,
|
|
1459
|
+
store,
|
|
1460
|
+
reporter: input.reporter,
|
|
1461
|
+
runReporter,
|
|
1462
|
+
force: Boolean(input.force),
|
|
1463
|
+
modelArtifact: input.modelArtifact,
|
|
1464
|
+
});
|
|
1465
|
+
await refreshArtifactManifest(prepared);
|
|
1466
|
+
if (stage === "candidate") {
|
|
1467
|
+
await completeRequestedStage(stage);
|
|
1468
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (stage === "score") {
|
|
1472
|
+
await runScoreStage({
|
|
1473
|
+
prepared,
|
|
1474
|
+
config: input.config,
|
|
1475
|
+
store,
|
|
1476
|
+
reporter: input.reporter,
|
|
1477
|
+
runReporter,
|
|
1478
|
+
});
|
|
1479
|
+
await refreshArtifactManifest(prepared);
|
|
1480
|
+
await completeRequestedStage(stage);
|
|
621
1481
|
return stageResult({ request: input.request, stage, artifacts });
|
|
1482
|
+
}
|
|
1483
|
+
if (stage === "report" || stage === "all") {
|
|
1484
|
+
report = await runReportStage({
|
|
1485
|
+
prepared,
|
|
1486
|
+
config: input.config,
|
|
1487
|
+
store,
|
|
1488
|
+
reporter: input.reporter,
|
|
1489
|
+
startedAt,
|
|
1490
|
+
startedPerf,
|
|
1491
|
+
emitReportingEvent: stage === "report",
|
|
1492
|
+
});
|
|
1493
|
+
return stageResult({ request: input.request, stage, artifacts, report });
|
|
1494
|
+
}
|
|
1495
|
+
throw new Error(`Unknown run stage: ${stage}`);
|
|
622
1496
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
store
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
const cancelled = error instanceof ProcessCancelledError
|
|
1499
|
+
|| await store.isCancellationRequested(input.request.run_id).catch(() => false);
|
|
1500
|
+
if (cancelled) {
|
|
1501
|
+
const state = await store.getRun(input.request.run_id).catch(() => null);
|
|
1502
|
+
if (state?.status !== "cancelled")
|
|
1503
|
+
await store.cancelRun(input.request.run_id).catch(() => undefined);
|
|
1504
|
+
await store.finalizeCancellation(input.request.run_id).catch(() => undefined);
|
|
1505
|
+
await input.reporter?.onEvent?.({
|
|
1506
|
+
stage: "cancelled",
|
|
1507
|
+
status: "cancelled",
|
|
1508
|
+
message: "Run cancelled.",
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
await store.failRun(input.request.run_id, error instanceof Error ? error.message : String(error)).catch(() => undefined);
|
|
1513
|
+
await input.reporter?.onEvent?.({
|
|
1514
|
+
stage: "failed",
|
|
1515
|
+
status: "failed",
|
|
1516
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
throw error;
|
|
635
1520
|
}
|
|
636
|
-
if (stage === "score") {
|
|
637
|
-
await runScoreStage({
|
|
638
|
-
prepared,
|
|
639
|
-
config: input.config,
|
|
640
|
-
store,
|
|
641
|
-
reporter: input.reporter,
|
|
642
|
-
runReporter,
|
|
643
|
-
});
|
|
644
|
-
return stageResult({ request: input.request, stage, artifacts });
|
|
645
|
-
}
|
|
646
|
-
if (stage === "report" || stage === "all") {
|
|
647
|
-
report = await runReportStage({
|
|
648
|
-
prepared,
|
|
649
|
-
store,
|
|
650
|
-
reporter: input.reporter,
|
|
651
|
-
startedAt,
|
|
652
|
-
startedPerf,
|
|
653
|
-
emitReportingEvent: stage === "report",
|
|
654
|
-
});
|
|
655
|
-
return stageResult({ request: input.request, stage, artifacts, report });
|
|
656
|
-
}
|
|
657
|
-
throw new Error(`Unknown run stage: ${stage}`);
|
|
658
1521
|
}
|
|
659
|
-
|
|
660
|
-
await
|
|
661
|
-
await
|
|
662
|
-
stage: "failed",
|
|
663
|
-
status: "failed",
|
|
664
|
-
message: error instanceof Error ? error.message : String(error),
|
|
665
|
-
});
|
|
666
|
-
throw error;
|
|
1522
|
+
finally {
|
|
1523
|
+
await releaseArtifactLock?.();
|
|
1524
|
+
await releaseRunLock();
|
|
667
1525
|
}
|
|
668
1526
|
}
|
|
669
1527
|
function stageResult(args) {
|
|
@@ -680,6 +1538,7 @@ function stageResult(args) {
|
|
|
680
1538
|
baseline_eval: args.artifacts.baselineEvalJson,
|
|
681
1539
|
candidate_eval: args.artifacts.candidateEvalJson,
|
|
682
1540
|
report: args.artifacts.runReportJson,
|
|
1541
|
+
artifact_manifest: args.artifacts.artifactManifestJson,
|
|
683
1542
|
},
|
|
684
1543
|
};
|
|
685
1544
|
}
|