@tuned-tensor/local 0.2.8 → 0.3.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/CHANGELOG.md +67 -0
- package/README.md +84 -204
- package/dist/artifacts.d.ts +3 -4
- package/dist/artifacts.js +55 -33
- package/dist/artifacts.js.map +1 -1
- package/dist/compare.d.ts +1 -8
- package/dist/compare.js +1 -23
- package/dist/compare.js.map +1 -1
- package/dist/contracts.d.ts +59 -366
- package/dist/contracts.js +73 -224
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +2 -4
- package/dist/dataset.js +31 -125
- package/dist/dataset.js.map +1 -1
- package/dist/doctor.d.ts +2 -3
- package/dist/doctor.js +23 -161
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +11 -71
- package/dist/evaluation.js +212 -572
- package/dist/evaluation.js.map +1 -1
- package/dist/huggingface-cache.d.ts +8 -7
- package/dist/huggingface-cache.js +16 -8
- package/dist/huggingface-cache.js.map +1 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.js +205 -628
- package/dist/index.js.map +1 -1
- package/dist/local-project.d.ts +1 -11
- package/dist/local-project.js +33 -61
- package/dist/local-project.js.map +1 -1
- package/dist/model-registry.d.ts +3 -16
- package/dist/model-registry.js +76 -292
- package/dist/model-registry.js.map +1 -1
- package/dist/model-server.d.ts +1 -2
- package/dist/model-server.js +9 -18
- package/dist/model-server.js.map +1 -1
- package/dist/orchestrator.d.ts +11 -25
- package/dist/orchestrator.js +246 -566
- package/dist/orchestrator.js.map +1 -1
- package/dist/prefetch.d.ts +1 -5
- package/dist/prefetch.js +14 -19
- package/dist/prefetch.js.map +1 -1
- package/dist/process-runner.d.ts +10 -29
- package/dist/process-runner.js +63 -169
- package/dist/process-runner.js.map +1 -1
- package/dist/process-training.d.ts +0 -1
- package/dist/process-training.js +10 -87
- package/dist/process-training.js.map +1 -1
- package/dist/store.d.ts +1 -3
- package/dist/store.js +92 -290
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +87 -152
- package/docs/spark.md +66 -97
- package/examples/dry-runner.json +14 -0
- package/examples/local-runner.json +8 -6
- package/examples/smoke-spec.json +41 -0
- package/package.json +6 -6
- package/training/local-runner/pyproject.toml +0 -5
- package/training/local-runner/src/evaluate.py +89 -234
- package/training/local-runner/src/model_contract.py +47 -0
- package/training/local-runner/src/prefetch.py +18 -4
- package/training/local-runner/src/serve.py +54 -115
- package/training/local-runner/src/sft_data.py +97 -0
- package/training/local-runner/src/train.py +146 -346
- package/training/local-runner/uv.lock +0 -1197
- package/dist/labeling-sanitize.d.ts +0 -31
- package/dist/labeling-sanitize.js +0 -158
- package/dist/labeling-sanitize.js.map +0 -1
- package/dist/labeling.d.ts +0 -155
- package/dist/labeling.js +0 -496
- package/dist/labeling.js.map +0 -1
- package/dist/openrouter.d.ts +0 -29
- package/dist/openrouter.js +0 -66
- package/dist/openrouter.js.map +0 -1
- package/dist/server.d.ts +0 -9
- package/dist/server.js +0 -211
- package/dist/server.js.map +0 -1
- package/docs/local-workflow-remediation-2026-07-13.md +0 -130
- package/docs/local-workflow-ux-review-2026-07-13.md +0 -458
- package/examples/dpo-preferences.jsonl +0 -2
- package/examples/dpo-run-request.json +0 -34
- package/examples/smoke-run-request.json +0 -34
- package/training/local-runner/src/train_dpo.py +0 -286
package/dist/orchestrator.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { basename, dirname,
|
|
6
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { defaultArtifactPrefix, fileUri, assertArtifactManifest, ARTIFACT_WORKFLOW_LOCK_FILE, claimRunArtifactDirectory, prepareRunDirectories, readJson, resolveRunArtifacts, writeArtifactManifest, writeFileAtomic, writeJsonAtomic, } from "./artifacts.js";
|
|
9
9
|
import { evalReportSchema, fineTuneRunRequestSchema, localRunnerConfigSchema, runReportSchema, trainingReportSchema, } from "./contracts.js";
|
|
10
|
-
import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec,
|
|
11
|
-
import { compareEvalReports, deriveSampleSeed, evaluateExamples,
|
|
10
|
+
import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec, normalizeChatJsonlForRelocation, } from "./dataset.js";
|
|
11
|
+
import { INFERENCE_PROTOCOL_VERSION, compareEvalReports, deriveSampleSeed, evaluateExamples, splitSpecExamples, } from "./evaluation.js";
|
|
12
12
|
import { launchProcessTraining } from "./process-training.js";
|
|
13
13
|
import { assertUsableModelArtifact, localModelArtifactPath } from "./model-registry.js";
|
|
14
14
|
import { ProcessCancelledError } from "./process-runner.js";
|
|
@@ -27,12 +27,9 @@ export async function loadLocalRunnerConfig(path) {
|
|
|
27
27
|
const configPath = resolve(path);
|
|
28
28
|
const base = dirname(configPath);
|
|
29
29
|
const config = localRunnerConfigSchema.parse(await loadJsonFile(configPath));
|
|
30
|
-
const configPathValue = (value
|
|
30
|
+
const configPathValue = (value) => {
|
|
31
31
|
if (!value)
|
|
32
32
|
return undefined;
|
|
33
|
-
if (preserveBundled && (value === "training/local-runner" || value.startsWith("training/local-runner/"))) {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
33
|
if (value === "~")
|
|
37
34
|
return homedir();
|
|
38
35
|
if (value.startsWith("~/"))
|
|
@@ -43,25 +40,10 @@ export async function loadLocalRunnerConfig(path) {
|
|
|
43
40
|
...config,
|
|
44
41
|
artifactRoot: configPathValue(config.artifactRoot),
|
|
45
42
|
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
43
|
paths: {
|
|
53
44
|
baseModel: configPathValue(config.paths.baseModel),
|
|
54
45
|
modelCache: configPathValue(config.paths.modelCache),
|
|
55
46
|
},
|
|
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
47
|
};
|
|
66
48
|
}
|
|
67
49
|
function elapsed(started) {
|
|
@@ -80,26 +62,146 @@ async function pathExists(path) {
|
|
|
80
62
|
return false;
|
|
81
63
|
}
|
|
82
64
|
}
|
|
83
|
-
function
|
|
65
|
+
function selectPrebuiltEvaluationSplit(dataset) {
|
|
84
66
|
if (!dataset)
|
|
85
67
|
throw new Error("dataset_prebuilt is required");
|
|
86
|
-
if (dataset.test)
|
|
87
|
-
return { path: stripFileUri(dataset.test), split: "prebuilt_test" };
|
|
88
68
|
if (dataset.validation)
|
|
89
|
-
return
|
|
90
|
-
|
|
69
|
+
return "prebuilt_validation";
|
|
70
|
+
if (dataset.test)
|
|
71
|
+
return "prebuilt_test";
|
|
72
|
+
throw new Error("dataset_prebuilt requires a distinct validation or test split");
|
|
91
73
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
function assertMatchingDatasetSystems(jsonl, expectedSystem, split) {
|
|
75
|
+
for (const [index, line] of jsonl.split(/\r?\n/).entries()) {
|
|
76
|
+
if (!line.trim())
|
|
77
|
+
continue;
|
|
78
|
+
const row = JSON.parse(line);
|
|
79
|
+
const actualSystem = row.messages.find((message) => message.role === "system")?.content ?? "";
|
|
80
|
+
if (actualSystem !== expectedSystem) {
|
|
81
|
+
throw new Error(`Prebuilt ${split} row ${index + 1} system message must match the behavior spec system message.`);
|
|
82
|
+
}
|
|
95
83
|
}
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
}
|
|
85
|
+
function addDryRunPlaceholders(request, dryRun) {
|
|
86
|
+
if (!dryRun || !request || typeof request !== "object" || Array.isArray(request))
|
|
87
|
+
return request;
|
|
88
|
+
const candidate = request;
|
|
89
|
+
if (candidate.dataset_prebuilt)
|
|
90
|
+
return request;
|
|
91
|
+
const snapshot = candidate.spec_snapshot;
|
|
92
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
|
|
93
|
+
return request;
|
|
94
|
+
const examples = snapshot.examples;
|
|
95
|
+
if (!Array.isArray(examples) || examples.length > 0)
|
|
96
|
+
return request;
|
|
97
|
+
return {
|
|
98
|
+
...candidate,
|
|
99
|
+
spec_snapshot: {
|
|
100
|
+
...snapshot,
|
|
101
|
+
examples: [
|
|
102
|
+
{ input: "Dry-run placeholder input A", output: "Dry-run placeholder output A" },
|
|
103
|
+
{ input: "Dry-run placeholder input B", output: "Dry-run placeholder output B" },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function validateDatasetInputs(request, config) {
|
|
109
|
+
const inputIdentity = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
|
|
110
|
+
const evalSampleSeed = config.evaluation.sampleSeed
|
|
111
|
+
?? deriveSampleSeed(request.behavior_spec_id);
|
|
112
|
+
if (!request.dataset_prebuilt) {
|
|
113
|
+
const inlineExamples = examplesFromSpec(request.spec_snapshot);
|
|
114
|
+
const seenInputs = new Map();
|
|
115
|
+
for (const [index, example] of inlineExamples.entries()) {
|
|
116
|
+
const identity = inputIdentity(example.input);
|
|
117
|
+
const previous = seenInputs.get(identity);
|
|
118
|
+
if (previous !== undefined) {
|
|
119
|
+
throw new Error(`Inline examples ${previous + 1} and ${index + 1} have duplicate inputs; `
|
|
120
|
+
+ "training and held-out evaluation require distinct prompts.");
|
|
121
|
+
}
|
|
122
|
+
seenInputs.set(identity, index);
|
|
123
|
+
}
|
|
124
|
+
if (!config.dryRun && inlineExamples.length < 2) {
|
|
125
|
+
throw new Error("A real local fine-tune requires at least 2 inline examples so training and evaluation use distinct data.");
|
|
126
|
+
}
|
|
127
|
+
const split = splitSpecExamples(inlineExamples, evalSampleSeed);
|
|
128
|
+
const hasHoldout = split.holdout.length > 0;
|
|
129
|
+
const trainingExamples = hasHoldout ? split.train : inlineExamples;
|
|
130
|
+
const evaluationExamples = hasHoldout ? split.holdout : inlineExamples;
|
|
131
|
+
return {
|
|
132
|
+
trainingJsonl: compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples }),
|
|
133
|
+
trainingExampleCount: trainingExamples.length,
|
|
134
|
+
evaluationExamples,
|
|
135
|
+
evalSplit: hasHoldout ? "spec_holdout" : "spec_examples",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const dataset = request.dataset_prebuilt;
|
|
139
|
+
const splitEntries = [
|
|
140
|
+
["training", dataset.training],
|
|
141
|
+
["validation", dataset.validation],
|
|
142
|
+
["test", dataset.test],
|
|
143
|
+
];
|
|
144
|
+
const normalized = new Map();
|
|
145
|
+
const expectedSystem = buildSystemMessage(request.spec_snapshot);
|
|
146
|
+
for (const [name, value] of splitEntries) {
|
|
147
|
+
if (!value)
|
|
148
|
+
continue;
|
|
149
|
+
const path = stripFileUri(value);
|
|
150
|
+
const jsonl = await normalizeChatJsonlForRelocation(path);
|
|
151
|
+
assertMatchingDatasetSystems(jsonl, expectedSystem, name);
|
|
152
|
+
normalized.set(name, {
|
|
153
|
+
jsonl,
|
|
154
|
+
examples: await examplesFromChatJsonl(path),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const evaluationSplit = selectPrebuiltEvaluationSplit(dataset);
|
|
158
|
+
const evaluationKey = evaluationSplit === "prebuilt_validation"
|
|
159
|
+
? "validation"
|
|
160
|
+
: "test";
|
|
161
|
+
const training = normalized.get("training");
|
|
162
|
+
const evaluationData = normalized.get(evaluationKey);
|
|
163
|
+
if (!training || !evaluationData) {
|
|
164
|
+
throw new Error("Unable to load the configured prebuilt training and evaluation datasets.");
|
|
165
|
+
}
|
|
166
|
+
const trainingInputs = new Set(training.examples.map((example) => inputIdentity(example.input)));
|
|
167
|
+
for (const splitName of ["validation", "test"]) {
|
|
168
|
+
const split = normalized.get(splitName);
|
|
169
|
+
if (!split)
|
|
170
|
+
continue;
|
|
171
|
+
const splitInputs = new Set();
|
|
172
|
+
for (const example of split.examples) {
|
|
173
|
+
const identity = inputIdentity(example.input);
|
|
174
|
+
if (splitInputs.has(identity)) {
|
|
175
|
+
throw new Error(`Prebuilt ${splitName} data contains duplicate inputs; `
|
|
176
|
+
+ "evaluation prompts must be unique so metrics and comparisons remain well-defined.");
|
|
177
|
+
}
|
|
178
|
+
splitInputs.add(identity);
|
|
179
|
+
}
|
|
180
|
+
const overlap = split.examples.filter((example) => trainingInputs.has(inputIdentity(example.input)));
|
|
181
|
+
if (overlap.length > 0) {
|
|
182
|
+
throw new Error(`Prebuilt training and ${splitName} data overlap on ${overlap.length} input(s); `
|
|
183
|
+
+ "evaluation examples must be held out from training.");
|
|
184
|
+
}
|
|
98
185
|
}
|
|
99
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
trainingJsonl: training.jsonl,
|
|
188
|
+
trainingExampleCount: training.examples.length,
|
|
189
|
+
evaluationExamples: evaluationData.examples,
|
|
190
|
+
evalSplit: evaluationSplit,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parses the public contracts and fully validates every dataset before a run
|
|
195
|
+
* lock, store record, or artifact-directory claim is created.
|
|
196
|
+
*/
|
|
197
|
+
export async function validateLocalFineTuneInput(input) {
|
|
198
|
+
const config = localRunnerConfigSchema.parse(input.config);
|
|
199
|
+
const request = fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun));
|
|
200
|
+
await validateDatasetInputs(request, config);
|
|
201
|
+
return { request, config };
|
|
100
202
|
}
|
|
101
203
|
function artifactPrefix(request) {
|
|
102
|
-
return
|
|
204
|
+
return defaultArtifactPrefix({
|
|
103
205
|
userId: request.user_id,
|
|
104
206
|
behaviorSpecId: request.behavior_spec_id,
|
|
105
207
|
runId: request.run_id,
|
|
@@ -134,33 +236,6 @@ async function packageVersion() {
|
|
|
134
236
|
return "unknown";
|
|
135
237
|
}
|
|
136
238
|
}
|
|
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
239
|
async function runtimeFingerprint() {
|
|
165
240
|
const bundledProject = resolve(packageRoot, "training/local-runner");
|
|
166
241
|
return hashJson({
|
|
@@ -179,14 +254,9 @@ function preparedSourceFingerprint(args) {
|
|
|
179
254
|
preparation_config: args.preparationConfig,
|
|
180
255
|
base_model_revision: args.baseModelRevision ?? null,
|
|
181
256
|
base_model_fingerprint: args.baseModelFingerprint ?? null,
|
|
182
|
-
parent_model_fingerprint: args.parentModelFingerprint ?? null,
|
|
183
257
|
dataset_fingerprints: args.datasetFingerprints,
|
|
184
258
|
});
|
|
185
259
|
}
|
|
186
|
-
async function countJsonlRows(path) {
|
|
187
|
-
const text = await readFile(path, "utf8");
|
|
188
|
-
return text.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
189
|
-
}
|
|
190
260
|
async function hashFile(path) {
|
|
191
261
|
const hash = createHash("sha256");
|
|
192
262
|
for await (const chunk of createReadStream(path))
|
|
@@ -206,21 +276,6 @@ async function datasetFingerprints(request) {
|
|
|
206
276
|
continue;
|
|
207
277
|
const datasetPath = stripFileUri(value);
|
|
208
278
|
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
|
-
}
|
|
224
279
|
}
|
|
225
280
|
return fingerprints;
|
|
226
281
|
}
|
|
@@ -246,44 +301,6 @@ async function resolveBaseModelRevision(request, config) {
|
|
|
246
301
|
return undefined;
|
|
247
302
|
}
|
|
248
303
|
}
|
|
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
304
|
/** Hash a local base model while permitting Hugging Face snapshot file links. */
|
|
288
305
|
export async function fingerprintLocalBaseModel(uri) {
|
|
289
306
|
const root = localModelArtifactPath(uri);
|
|
@@ -331,8 +348,6 @@ function statusForProgressStage(stage) {
|
|
|
331
348
|
return "training";
|
|
332
349
|
if (stage === "preparing")
|
|
333
350
|
return "preparing";
|
|
334
|
-
if (stage === "scoring")
|
|
335
|
-
return "scoring";
|
|
336
351
|
if (stage === "reporting")
|
|
337
352
|
return "reporting";
|
|
338
353
|
return "training";
|
|
@@ -348,9 +363,6 @@ async function readStageMetadata(path) {
|
|
|
348
363
|
return null;
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
|
-
async function clearDependentStageArtifacts(artifacts) {
|
|
352
|
-
await cleanupStageArtifacts(artifacts, "prepare");
|
|
353
|
-
}
|
|
354
366
|
async function removePrefixedArtifacts(path) {
|
|
355
367
|
const directory = dirname(path);
|
|
356
368
|
const prefix = `${basename(path)}.`;
|
|
@@ -489,54 +501,29 @@ async function acquireArtifactLock(artifacts) {
|
|
|
489
501
|
function isDryTraining(training) {
|
|
490
502
|
return training.metrics?.dry_run === true;
|
|
491
503
|
}
|
|
492
|
-
function stringMetadata(value, key) {
|
|
493
|
-
const entry = value?.[key];
|
|
494
|
-
return typeof entry === "string" && entry.length > 0 ? entry : undefined;
|
|
495
|
-
}
|
|
496
504
|
async function modelManifestContract(prepared, training) {
|
|
497
505
|
if (isDryTraining(training) || !training.model_artifact_uri)
|
|
498
506
|
return undefined;
|
|
499
|
-
const
|
|
500
|
-
const explicitContract = Boolean(metadata?.framework && metadata?.format);
|
|
501
|
-
const inspection = await assertUsableModelArtifact(training.model_artifact_uri, {
|
|
502
|
-
allowUnrecognizedPayload: explicitContract,
|
|
503
|
-
});
|
|
507
|
+
const inspection = await assertUsableModelArtifact(training.model_artifact_uri);
|
|
504
508
|
const adapterWeights = inspection.adapter_weight_file_count > 0
|
|
505
509
|
&& inspection.adapter_weight_bytes > 0;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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.`);
|
|
510
|
+
if (!adapterWeights || !inspection.has_adapter_config) {
|
|
511
|
+
throw new Error(`Local training must produce a PEFT adapter with adapter_model.safetensors or adapter_model.bin and `
|
|
512
|
+
+ `a non-empty adapter_config.json: ${inspection.path}`);
|
|
519
513
|
}
|
|
520
|
-
const
|
|
521
|
-
const format = metadata?.format
|
|
522
|
-
?? (inspection.kind === "file" && inspection.path.endsWith(".tar.gz")
|
|
523
|
-
? "tar.gz"
|
|
524
|
-
: inspection.kind === "directory" ? "huggingface-directory" : "file");
|
|
514
|
+
const format = inspection.kind === "file" ? "tar.gz" : "huggingface-directory";
|
|
525
515
|
return {
|
|
526
516
|
artifact_kind: inspection.kind,
|
|
527
517
|
format,
|
|
528
|
-
framework:
|
|
518
|
+
framework: "transformers-peft",
|
|
529
519
|
base_model: prepared.request.spec_snapshot.base_model,
|
|
530
|
-
base_model_revision:
|
|
531
|
-
?? stringMetadata(training.metrics, "base_model_revision")
|
|
532
|
-
?? prepared.metadata.base_model_revision
|
|
520
|
+
base_model_revision: prepared.metadata.base_model_revision
|
|
533
521
|
?? prepared.request.hyperparameters.base_model_revision,
|
|
534
522
|
base_model_artifact_uri: training.base_model_artifact_uri,
|
|
535
523
|
base_model_fingerprint: prepared.metadata.base_model_fingerprint ?? undefined,
|
|
536
|
-
parent_model_artifact: prepared.request.hyperparameters.parent_model_artifact,
|
|
537
524
|
artifact_uri: training.model_artifact_uri,
|
|
538
525
|
artifact_root: inspection.path,
|
|
539
|
-
servable:
|
|
526
|
+
servable: true,
|
|
540
527
|
};
|
|
541
528
|
}
|
|
542
529
|
async function refreshArtifactManifest(prepared) {
|
|
@@ -557,37 +544,19 @@ function stageOutputPath(prepared, stage) {
|
|
|
557
544
|
function stageFingerprintPath(prepared, stage) {
|
|
558
545
|
return `${stageOutputPath(prepared, stage)}.stage.json`;
|
|
559
546
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
}
|
|
547
|
+
async function runnerFileFingerprints() {
|
|
548
|
+
const project = resolve(packageRoot, "training/local-runner");
|
|
549
|
+
const bundledSource = resolve(packageRoot, "training/local-runner/src");
|
|
550
|
+
const pythonSources = (await readdir(bundledSource, { withFileTypes: true }))
|
|
551
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".py"))
|
|
552
|
+
.map((entry) => resolve(bundledSource, entry.name));
|
|
553
|
+
const candidates = [...new Set([
|
|
554
|
+
...pythonSources,
|
|
555
|
+
resolve(project, "pyproject.toml"),
|
|
556
|
+
resolve(project, "uv.lock"),
|
|
557
|
+
])];
|
|
589
558
|
const fingerprints = {};
|
|
590
|
-
for (const path of
|
|
559
|
+
for (const path of candidates.sort())
|
|
591
560
|
fingerprints[path] = await hashFileIfPresent(path);
|
|
592
561
|
return fingerprints;
|
|
593
562
|
}
|
|
@@ -598,45 +567,20 @@ async function stageFingerprint(args) {
|
|
|
598
567
|
dry_run: args.config.dryRun,
|
|
599
568
|
};
|
|
600
569
|
if (args.stage === "train") {
|
|
601
|
-
const entrypointFiles = await
|
|
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
|
-
});
|
|
570
|
+
const entrypointFiles = await runnerFileFingerprints();
|
|
612
571
|
return hashJson({
|
|
613
572
|
...common,
|
|
614
|
-
training: args.config.training,
|
|
615
573
|
paths: args.config.paths,
|
|
616
574
|
entrypoint_files: entrypointFiles,
|
|
617
575
|
});
|
|
618
576
|
}
|
|
619
|
-
const
|
|
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
|
-
});
|
|
577
|
+
const entrypointFiles = await runnerFileFingerprints();
|
|
631
578
|
const evaluation = {
|
|
632
|
-
|
|
633
|
-
? args.config.evaluation.baselineCommand
|
|
634
|
-
: args.config.evaluation.candidateCommand,
|
|
579
|
+
inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
635
580
|
inference: args.config.evaluation.inference,
|
|
636
581
|
scoring: args.config.evaluation.scoring,
|
|
637
582
|
timeout_ms: args.config.evaluation.timeoutMs,
|
|
638
583
|
baseline_cache: args.config.evaluation.baselineCache,
|
|
639
|
-
llm: args.config.llm,
|
|
640
584
|
model_cache: args.config.paths.modelCache,
|
|
641
585
|
};
|
|
642
586
|
return hashJson({
|
|
@@ -739,64 +683,19 @@ async function ensureRunRecord(args) {
|
|
|
739
683
|
}
|
|
740
684
|
async function computePreparedRun(args) {
|
|
741
685
|
const { request, config, artifacts } = args;
|
|
742
|
-
const evalSampleSeed = config.evaluation.sampleSeed
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (
|
|
747
|
-
|
|
748
|
-
throw new Error("DPO training requires dataset_prebuilt.");
|
|
749
|
-
const trainingPath = stripFileUri(request.dataset_prebuilt.training);
|
|
750
|
-
if (args.writeArtifacts)
|
|
751
|
-
await copyFile(trainingPath, artifacts.trainingJsonl);
|
|
752
|
-
trainingExampleCount = await countJsonlRows(trainingPath);
|
|
753
|
-
const evaluation = selectDpoEvaluation(request);
|
|
754
|
-
evalSplit = evaluation.split;
|
|
755
|
-
examples = evaluation.path
|
|
756
|
-
? await examplesFromChatJsonl(evaluation.path)
|
|
757
|
-
: examplesFromSpec(request.spec_snapshot);
|
|
758
|
-
}
|
|
759
|
-
else if (request.dataset_prebuilt) {
|
|
760
|
-
const trainingPath = stripFileUri(request.dataset_prebuilt.training);
|
|
761
|
-
const evaluation = selectPrebuiltEvaluation(request.dataset_prebuilt);
|
|
762
|
-
evalSplit = evaluation.split;
|
|
763
|
-
if (evalSplit === "prebuilt_training" && !config.dryRun && !config.evaluation.allowPrebuiltTrainingEval) {
|
|
764
|
-
throw new Error("dataset_prebuilt has no test or validation split, so evaluation would run on the training data and "
|
|
765
|
-
+ "overstate improvement. Provide dataset_prebuilt.test or dataset_prebuilt.validation, or set "
|
|
766
|
-
+ "evaluation.allowPrebuiltTrainingEval=true to evaluate on the training split anyway.");
|
|
767
|
-
}
|
|
768
|
-
if (args.writeArtifacts) {
|
|
769
|
-
const normalizedTraining = await normalizeChatJsonlForRelocation(trainingPath);
|
|
770
|
-
await writeFile(artifacts.trainingJsonl, `${normalizedTraining}\n`, "utf8");
|
|
771
|
-
}
|
|
772
|
-
examples = await examplesFromChatJsonl(evaluation.path);
|
|
773
|
-
trainingExampleCount = null;
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
const split = splitSpecExamples(request.spec_snapshot.examples, evalSampleSeed);
|
|
777
|
-
let trainingExamples = request.spec_snapshot.examples;
|
|
778
|
-
if (split.holdout.length > 0) {
|
|
779
|
-
trainingExamples = split.train;
|
|
780
|
-
examples = split.holdout;
|
|
781
|
-
evalSplit = "spec_holdout";
|
|
782
|
-
}
|
|
783
|
-
trainingExampleCount = trainingExamples.length;
|
|
784
|
-
if (args.writeArtifacts) {
|
|
785
|
-
const jsonl = compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples });
|
|
786
|
-
await writeFile(artifacts.trainingJsonl, `${jsonl}\n`, "utf8");
|
|
787
|
-
}
|
|
686
|
+
const evalSampleSeed = config.evaluation.sampleSeed
|
|
687
|
+
?? deriveSampleSeed(request.behavior_spec_id);
|
|
688
|
+
const dataset = await validateDatasetInputs(request, config);
|
|
689
|
+
const examples = dataset.evaluationExamples;
|
|
690
|
+
if (args.writeArtifacts) {
|
|
691
|
+
await writeFileAtomic(artifacts.trainingJsonl, `${dataset.trainingJsonl}\n`);
|
|
788
692
|
}
|
|
789
693
|
const system = buildSystemMessage(request.spec_snapshot);
|
|
790
694
|
const baseModelForEvaluation = config.paths.baseModel ?? request.spec_snapshot.base_model;
|
|
791
|
-
const parentModelArtifact = request.hyperparameters.parent_model_artifact;
|
|
792
|
-
const parentModelFingerprint = parentModelArtifact
|
|
793
|
-
? await fingerprintLocalArtifact(parentModelArtifact)
|
|
794
|
-
: undefined;
|
|
795
695
|
const maxEvalExamples = config.evaluation.maxExamples ?? request.hyperparameters.max_eval_examples;
|
|
796
696
|
const evalExamplesUsed = Math.min(maxEvalExamples ?? examples.length, examples.length);
|
|
797
697
|
const fingerprints = await datasetFingerprints(request);
|
|
798
698
|
const requestFingerprint = hashJson(request);
|
|
799
|
-
const effectiveConfigFingerprintValue = await effectiveConfigFingerprint(config);
|
|
800
699
|
const runtimeFingerprintValue = await runtimeFingerprint();
|
|
801
700
|
const baseModelRevision = await resolveBaseModelRevision(request, config);
|
|
802
701
|
const baseModelFingerprint = config.paths.baseModel
|
|
@@ -806,9 +705,7 @@ async function computePreparedRun(args) {
|
|
|
806
705
|
run_id: request.run_id,
|
|
807
706
|
behavior_spec_id: request.behavior_spec_id,
|
|
808
707
|
user_id: request.user_id,
|
|
809
|
-
training_method: request.training_method,
|
|
810
708
|
request_fingerprint: requestFingerprint,
|
|
811
|
-
effective_config_fingerprint: effectiveConfigFingerprintValue,
|
|
812
709
|
runtime_fingerprint: runtimeFingerprintValue,
|
|
813
710
|
source_fingerprint: preparedSourceFingerprint({
|
|
814
711
|
requestFingerprint,
|
|
@@ -818,19 +715,17 @@ async function computePreparedRun(args) {
|
|
|
818
715
|
base_model_path: config.paths.baseModel,
|
|
819
716
|
max_eval_examples: config.evaluation.maxExamples,
|
|
820
717
|
eval_sample_seed: config.evaluation.sampleSeed,
|
|
821
|
-
allow_prebuilt_training_eval: config.evaluation.allowPrebuiltTrainingEval,
|
|
822
718
|
},
|
|
823
719
|
baseModelRevision,
|
|
824
720
|
baseModelFingerprint,
|
|
825
|
-
parentModelFingerprint,
|
|
826
721
|
datasetFingerprints: fingerprints,
|
|
827
722
|
}),
|
|
828
|
-
eval_split: evalSplit,
|
|
723
|
+
eval_split: dataset.evalSplit,
|
|
829
724
|
eval_sample_seed: evalSampleSeed,
|
|
830
725
|
eval_examples_total: examples.length,
|
|
831
726
|
eval_examples_used: evalExamplesUsed,
|
|
832
727
|
max_eval_examples: maxEvalExamples ?? null,
|
|
833
|
-
training_example_count: trainingExampleCount,
|
|
728
|
+
training_example_count: dataset.trainingExampleCount,
|
|
834
729
|
dataset_prebuilt: Boolean(request.dataset_prebuilt),
|
|
835
730
|
dataset_format: request.dataset_prebuilt?.format ?? null,
|
|
836
731
|
dataset_fingerprints: fingerprints,
|
|
@@ -838,8 +733,6 @@ async function computePreparedRun(args) {
|
|
|
838
733
|
base_model_for_evaluation: baseModelForEvaluation,
|
|
839
734
|
base_model_revision: baseModelRevision ?? null,
|
|
840
735
|
base_model_fingerprint: baseModelFingerprint ?? null,
|
|
841
|
-
parent_model_artifact: parentModelArtifact ?? null,
|
|
842
|
-
parent_model_fingerprint: parentModelFingerprint ?? null,
|
|
843
736
|
system_prompt_sha256: createHash("sha256").update(system).digest("hex"),
|
|
844
737
|
prepared_at: new Date().toISOString(),
|
|
845
738
|
};
|
|
@@ -865,7 +758,6 @@ async function prepareStage(args) {
|
|
|
865
758
|
? await readStageMetadata(args.artifacts.stageMetadataJson)
|
|
866
759
|
: null;
|
|
867
760
|
let canReuse = preparedExists
|
|
868
|
-
&& !args.force
|
|
869
761
|
&& existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
870
762
|
if (canReuse) {
|
|
871
763
|
canReuse = await verifyReusableArtifacts(args.artifacts, [
|
|
@@ -893,28 +785,17 @@ async function prepareStage(args) {
|
|
|
893
785
|
});
|
|
894
786
|
await throwIfCancelled(args.store, args.request);
|
|
895
787
|
await args.store.invalidateRunOutputs(args.request.run_id, { report: true, model: true });
|
|
896
|
-
await
|
|
788
|
+
await cleanupStageArtifacts(args.artifacts, "prepare");
|
|
897
789
|
const refreshed = await computePreparedRun({ ...args, writeArtifacts: true });
|
|
898
790
|
await refreshArtifactManifest(refreshed);
|
|
899
791
|
return refreshed;
|
|
900
792
|
}
|
|
901
|
-
async function ensurePrepared(args) {
|
|
902
|
-
return prepareStage({
|
|
903
|
-
request: args.request,
|
|
904
|
-
config: args.config,
|
|
905
|
-
artifacts: args.artifacts,
|
|
906
|
-
store: args.store,
|
|
907
|
-
reporter: args.reporter,
|
|
908
|
-
force: args.forcePrepare,
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
793
|
async function runBaselineStage(args) {
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
})) {
|
|
794
|
+
if (await canReuseStageArtifact({
|
|
795
|
+
stage: "baseline",
|
|
796
|
+
prepared: args.prepared,
|
|
797
|
+
config: args.config,
|
|
798
|
+
})) {
|
|
918
799
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
919
800
|
await updateRun({
|
|
920
801
|
store: args.store,
|
|
@@ -943,19 +824,17 @@ async function runBaselineStage(args) {
|
|
|
943
824
|
examples: args.prepared.examples.length,
|
|
944
825
|
eval_examples_used: args.prepared.metadata.eval_examples_used,
|
|
945
826
|
eval_split: args.prepared.metadata.eval_split,
|
|
946
|
-
model_id: args.prepared.
|
|
947
|
-
parent_model_artifact: args.prepared.metadata.parent_model_artifact,
|
|
827
|
+
model_id: args.prepared.baseModelForEvaluation,
|
|
948
828
|
},
|
|
949
829
|
});
|
|
950
830
|
const report = await evaluateExamples({
|
|
951
831
|
kind: "baseline",
|
|
952
|
-
modelId: args.prepared.
|
|
832
|
+
modelId: args.prepared.baseModelForEvaluation,
|
|
953
833
|
baseModelId: args.prepared.baseModelForEvaluation,
|
|
954
834
|
baseModelRevision: args.config.paths.baseModel
|
|
955
835
|
? undefined
|
|
956
836
|
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
957
|
-
sourceFingerprint: args.prepared.metadata.
|
|
958
|
-
adapterPath: args.prepared.metadata.parent_model_artifact ?? undefined,
|
|
837
|
+
sourceFingerprint: args.prepared.metadata.base_model_fingerprint ?? undefined,
|
|
959
838
|
examples: args.prepared.examples,
|
|
960
839
|
system: args.prepared.system,
|
|
961
840
|
config: args.config,
|
|
@@ -971,13 +850,12 @@ async function runBaselineStage(args) {
|
|
|
971
850
|
return report;
|
|
972
851
|
}
|
|
973
852
|
async function runTrainStage(args) {
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
})) {
|
|
853
|
+
if (await canReuseStageArtifact({
|
|
854
|
+
stage: "train",
|
|
855
|
+
prepared: args.prepared,
|
|
856
|
+
config: args.config,
|
|
857
|
+
verifyModel: true,
|
|
858
|
+
})) {
|
|
981
859
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
982
860
|
await updateRun({
|
|
983
861
|
store: args.store,
|
|
@@ -1011,7 +889,7 @@ async function runTrainStage(args) {
|
|
|
1011
889
|
status: "training",
|
|
1012
890
|
stage: "training",
|
|
1013
891
|
message: args.config.dryRun ? "Recording dry-run training result." : "Launching local training process.",
|
|
1014
|
-
details: { training_backend:
|
|
892
|
+
details: { training_backend: "local-uv", dry_run: args.config.dryRun },
|
|
1015
893
|
});
|
|
1016
894
|
const training = await launchProcessTraining({
|
|
1017
895
|
request: args.prepared.request,
|
|
@@ -1043,27 +921,9 @@ async function runTrainStage(args) {
|
|
|
1043
921
|
}
|
|
1044
922
|
return training;
|
|
1045
923
|
}
|
|
1046
|
-
async function writeExternalTrainingReport(args) {
|
|
1047
|
-
const training = trainingReportSchema.parse({
|
|
1048
|
-
provider: args.config.training.backend === "command" ? "local-command" : "local-uv",
|
|
1049
|
-
training_job_name: `external-${args.prepared.request.run_id}`,
|
|
1050
|
-
model_artifact_uri: fileUri(localModelArtifactPath(args.modelArtifact)),
|
|
1051
|
-
base_model_artifact_uri: args.config.paths.baseModel ? fileUri(args.config.paths.baseModel) : undefined,
|
|
1052
|
-
parent_model_artifact_uri: args.prepared.metadata.parent_model_artifact ?? undefined,
|
|
1053
|
-
artifact_metadata: {
|
|
1054
|
-
...(args.config.training.artifact ?? {}),
|
|
1055
|
-
notes: args.config.training.artifact?.notes ?? "External model artifact supplied with --model-artifact.",
|
|
1056
|
-
},
|
|
1057
|
-
metrics: { external_model_artifact: true },
|
|
1058
|
-
exit_code: null,
|
|
1059
|
-
log_uri: fileUri(args.prepared.artifacts.trainingReportJson),
|
|
1060
|
-
});
|
|
1061
|
-
await writeJsonAtomic(args.prepared.artifacts.trainingReportJson, training);
|
|
1062
|
-
return training;
|
|
1063
|
-
}
|
|
1064
924
|
async function runCandidateStage(args) {
|
|
1065
925
|
let verifiedTraining;
|
|
1066
|
-
if (
|
|
926
|
+
if (await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
1067
927
|
const trainingCurrent = await canReuseStageArtifact({
|
|
1068
928
|
stage: "train",
|
|
1069
929
|
prepared: args.prepared,
|
|
@@ -1074,9 +934,7 @@ async function runCandidateStage(args) {
|
|
|
1074
934
|
verifiedTraining = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
1075
935
|
}
|
|
1076
936
|
}
|
|
1077
|
-
if (
|
|
1078
|
-
&& !args.modelArtifact
|
|
1079
|
-
&& verifiedTraining
|
|
937
|
+
if (verifiedTraining
|
|
1080
938
|
&& await canReuseStageArtifact({
|
|
1081
939
|
stage: "candidate",
|
|
1082
940
|
prepared: args.prepared,
|
|
@@ -1110,33 +968,16 @@ async function runCandidateStage(args) {
|
|
|
1110
968
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
1111
969
|
return report;
|
|
1112
970
|
}
|
|
1113
|
-
if (!
|
|
1114
|
-
throw new Error("
|
|
971
|
+
if (!verifiedTraining) {
|
|
972
|
+
throw new Error("Candidate evaluation requires current verified training output.");
|
|
1115
973
|
}
|
|
1116
974
|
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
|
-
});
|
|
975
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1121
976
|
await cleanupStageArtifacts(args.prepared.artifacts, "candidate");
|
|
1122
|
-
|
|
1123
|
-
if (args.modelArtifact) {
|
|
1124
|
-
training = await writeExternalTrainingReport({
|
|
1125
|
-
prepared: args.prepared,
|
|
1126
|
-
config: args.config,
|
|
1127
|
-
modelArtifact: args.modelArtifact,
|
|
1128
|
-
});
|
|
1129
|
-
await writeStageFingerprint({ stage: "train", prepared: args.prepared, config: args.config });
|
|
1130
|
-
}
|
|
1131
|
-
else if (verifiedTraining) {
|
|
1132
|
-
training = verifiedTraining;
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
throw new Error("candidate stage requires training output or --model-artifact.");
|
|
1136
|
-
}
|
|
977
|
+
const training = verifiedTraining;
|
|
1137
978
|
const modelArtifact = training.model_artifact_uri;
|
|
1138
979
|
if (!modelArtifact)
|
|
1139
|
-
throw new Error("candidate stage requires a model_artifact_uri in training-report.json
|
|
980
|
+
throw new Error("candidate stage requires a model_artifact_uri in training-report.json.");
|
|
1140
981
|
if (!isDryTraining(training)) {
|
|
1141
982
|
await modelManifestContract(args.prepared, training);
|
|
1142
983
|
await refreshArtifactManifest(args.prepared);
|
|
@@ -1183,82 +1024,15 @@ async function runCandidateStage(args) {
|
|
|
1183
1024
|
});
|
|
1184
1025
|
return report;
|
|
1185
1026
|
}
|
|
1186
|
-
async function runScoreStage(args) {
|
|
1187
|
-
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
1188
|
-
throw new Error("score stage requires baseline-eval.json. Run --stage baseline first.");
|
|
1189
|
-
}
|
|
1190
|
-
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
1191
|
-
throw new Error("score stage requires candidate-eval.json. Run --stage candidate first.");
|
|
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");
|
|
1202
|
-
await updateRun({
|
|
1203
|
-
store: args.store,
|
|
1204
|
-
reporter: args.reporter,
|
|
1205
|
-
request: args.prepared.request,
|
|
1206
|
-
status: "scoring",
|
|
1207
|
-
stage: "scoring",
|
|
1208
|
-
message: "Rescoring existing baseline and candidate outputs.",
|
|
1209
|
-
details: { scoring_mode: args.config.evaluation.scoring.mode },
|
|
1210
|
-
});
|
|
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
|
-
}
|
|
1252
|
-
}
|
|
1253
1027
|
async function runReportStage(args) {
|
|
1254
1028
|
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
1255
|
-
throw new Error("
|
|
1029
|
+
throw new Error("Run reporting requires baseline-eval.json.");
|
|
1256
1030
|
}
|
|
1257
1031
|
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
1258
|
-
throw new Error("
|
|
1032
|
+
throw new Error("Run reporting requires candidate-eval.json.");
|
|
1259
1033
|
}
|
|
1260
1034
|
if (!await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
1261
|
-
throw new Error("
|
|
1035
|
+
throw new Error("Run reporting requires training-report.json.");
|
|
1262
1036
|
}
|
|
1263
1037
|
const currentTraining = await canReuseStageArtifact({
|
|
1264
1038
|
stage: "train",
|
|
@@ -1291,17 +1065,15 @@ async function runReportStage(args) {
|
|
|
1291
1065
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
1292
1066
|
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1293
1067
|
await cleanupStageArtifacts(args.prepared.artifacts, "report");
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1068
|
+
await updateRun({
|
|
1069
|
+
store: args.store,
|
|
1070
|
+
reporter: args.reporter,
|
|
1071
|
+
request: args.prepared.request,
|
|
1072
|
+
status: "reporting",
|
|
1073
|
+
stage: "reporting",
|
|
1074
|
+
message: "Writing run report.",
|
|
1075
|
+
details: { report_path: args.prepared.artifacts.runReportJson },
|
|
1076
|
+
});
|
|
1305
1077
|
const baseline = evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
|
|
1306
1078
|
const candidate = evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
|
|
1307
1079
|
const training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
@@ -1329,8 +1101,6 @@ async function runReportStage(args) {
|
|
|
1329
1101
|
run_metadata: {
|
|
1330
1102
|
base_model: args.prepared.request.spec_snapshot.base_model,
|
|
1331
1103
|
fine_tuned_model_id: training.model_artifact_uri ?? training.training_job_name,
|
|
1332
|
-
parent_model_artifact: args.prepared.metadata.parent_model_artifact,
|
|
1333
|
-
training_method: args.prepared.request.training_method,
|
|
1334
1104
|
dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
|
|
1335
1105
|
dataset_format: args.prepared.metadata.dataset_format,
|
|
1336
1106
|
dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
@@ -1367,10 +1137,14 @@ async function runReportStage(args) {
|
|
|
1367
1137
|
});
|
|
1368
1138
|
return report;
|
|
1369
1139
|
}
|
|
1370
|
-
export async function
|
|
1140
|
+
export async function runLocalFineTune(input) {
|
|
1141
|
+
const validated = await validateLocalFineTuneInput({
|
|
1142
|
+
request: input.request,
|
|
1143
|
+
config: input.config,
|
|
1144
|
+
});
|
|
1145
|
+
input = { ...input, ...validated };
|
|
1371
1146
|
const startedPerf = performance.now();
|
|
1372
1147
|
const startedAt = new Date().toISOString();
|
|
1373
|
-
const stage = input.stage ?? "all";
|
|
1374
1148
|
const prefix = artifactPrefix(input.request);
|
|
1375
1149
|
const artifacts = resolveRunArtifacts({ artifactRoot: input.config.artifactRoot, prefix });
|
|
1376
1150
|
const store = createLocalStore(input.config.storeRoot);
|
|
@@ -1389,110 +1163,51 @@ export async function runLocalFineTuneStage(input) {
|
|
|
1389
1163
|
let prepared;
|
|
1390
1164
|
try {
|
|
1391
1165
|
await throwIfCancelled(store, input.request);
|
|
1392
|
-
prepared = await
|
|
1166
|
+
prepared = await prepareStage({
|
|
1393
1167
|
request: input.request,
|
|
1394
1168
|
config: input.config,
|
|
1395
1169
|
artifacts,
|
|
1396
1170
|
store,
|
|
1397
1171
|
reporter: input.reporter,
|
|
1398
|
-
forcePrepare: Boolean(input.force && (stage === "prepare" || stage === "all")),
|
|
1399
1172
|
});
|
|
1400
1173
|
await throwIfCancelled(store, input.request);
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1174
|
+
await runBaselineStage({
|
|
1175
|
+
prepared,
|
|
1176
|
+
config: input.config,
|
|
1177
|
+
store,
|
|
1178
|
+
reporter: input.reporter,
|
|
1179
|
+
runReporter,
|
|
1180
|
+
});
|
|
1181
|
+
await refreshArtifactManifest(prepared);
|
|
1182
|
+
await runTrainStage({
|
|
1183
|
+
prepared,
|
|
1184
|
+
config: input.config,
|
|
1185
|
+
store,
|
|
1186
|
+
reporter: input.reporter,
|
|
1187
|
+
runReporter,
|
|
1188
|
+
});
|
|
1189
|
+
await runCandidateStage({
|
|
1190
|
+
prepared,
|
|
1191
|
+
config: input.config,
|
|
1192
|
+
store,
|
|
1193
|
+
reporter: input.reporter,
|
|
1194
|
+
runReporter,
|
|
1195
|
+
});
|
|
1196
|
+
await refreshArtifactManifest(prepared);
|
|
1197
|
+
const report = await runReportStage({
|
|
1198
|
+
prepared,
|
|
1199
|
+
config: input.config,
|
|
1200
|
+
store,
|
|
1201
|
+
reporter: input.reporter,
|
|
1202
|
+
startedAt,
|
|
1203
|
+
startedPerf,
|
|
1204
|
+
});
|
|
1205
|
+
return {
|
|
1206
|
+
request: input.request,
|
|
1207
|
+
report,
|
|
1208
|
+
reportPath: artifacts.runReportJson,
|
|
1209
|
+
artifactDir: artifacts.runDir,
|
|
1421
1210
|
};
|
|
1422
|
-
if (stage === "prepare") {
|
|
1423
|
-
await completeRequestedStage(stage);
|
|
1424
|
-
return stageResult({ request: input.request, stage, artifacts });
|
|
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);
|
|
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}`);
|
|
1496
1211
|
}
|
|
1497
1212
|
catch (error) {
|
|
1498
1213
|
const cancelled = error instanceof ProcessCancelledError
|
|
@@ -1524,39 +1239,4 @@ export async function runLocalFineTuneStage(input) {
|
|
|
1524
1239
|
await releaseRunLock();
|
|
1525
1240
|
}
|
|
1526
1241
|
}
|
|
1527
|
-
function stageResult(args) {
|
|
1528
|
-
return {
|
|
1529
|
-
request: args.request,
|
|
1530
|
-
stage: args.stage,
|
|
1531
|
-
report: args.report,
|
|
1532
|
-
reportPath: args.report ? args.artifacts.runReportJson : undefined,
|
|
1533
|
-
artifactDir: dirname(args.artifacts.runReportJson),
|
|
1534
|
-
artifacts: {
|
|
1535
|
-
training_jsonl: args.artifacts.trainingJsonl,
|
|
1536
|
-
stage_metadata: args.artifacts.stageMetadataJson,
|
|
1537
|
-
training_report: args.artifacts.trainingReportJson,
|
|
1538
|
-
baseline_eval: args.artifacts.baselineEvalJson,
|
|
1539
|
-
candidate_eval: args.artifacts.candidateEvalJson,
|
|
1540
|
-
report: args.artifacts.runReportJson,
|
|
1541
|
-
artifact_manifest: args.artifacts.artifactManifestJson,
|
|
1542
|
-
},
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
export async function runLocalFineTune(input) {
|
|
1546
|
-
const result = await runLocalFineTuneStage({
|
|
1547
|
-
request: input.request,
|
|
1548
|
-
config: input.config,
|
|
1549
|
-
reporter: input.reporter,
|
|
1550
|
-
stage: "all",
|
|
1551
|
-
});
|
|
1552
|
-
if (!result.report || !result.reportPath) {
|
|
1553
|
-
throw new Error("Full local fine-tune run did not produce a report.");
|
|
1554
|
-
}
|
|
1555
|
-
return {
|
|
1556
|
-
request: result.request,
|
|
1557
|
-
report: result.report,
|
|
1558
|
-
reportPath: result.reportPath,
|
|
1559
|
-
artifactDir: result.artifactDir,
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
1242
|
//# sourceMappingURL=orchestrator.js.map
|