@tuned-tensor/local 0.2.9 → 0.4.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 +86 -0
- package/README.md +110 -201
- package/dist/active-model.d.ts +16 -0
- package/dist/active-model.js +88 -0
- package/dist/active-model.js.map +1 -0
- package/dist/artifacts.d.ts +5 -4
- package/dist/artifacts.js +57 -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 +234 -365
- package/dist/contracts.js +95 -224
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +11 -4
- package/dist/dataset.js +67 -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/general-regression.d.ts +10 -0
- package/dist/general-regression.js +16 -0
- package/dist/general-regression.js.map +1 -0
- 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 +2 -10
- package/dist/index.js +358 -627
- 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 +7 -3
- package/dist/model-server.js +34 -25
- package/dist/model-server.js.map +1 -1
- package/dist/orchestrator.d.ts +11 -25
- package/dist/orchestrator.js +393 -563
- 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/general-regression.jsonl +4 -0
- package/examples/local-runner.json +13 -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 +60 -117
- 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,20 +1,21 @@
|
|
|
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, evaluationSuiteFromChatJsonl, 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";
|
|
15
15
|
import { createLocalStore } from "./store.js";
|
|
16
16
|
import { withHuggingFaceCacheEnvironment } from "./huggingface-cache.js";
|
|
17
17
|
import { verifyLocalBaseModel } from "./prefetch.js";
|
|
18
|
+
import { evaluateGeneralRegressionGate } from "./general-regression.js";
|
|
18
19
|
export async function loadJsonFile(path) {
|
|
19
20
|
return JSON.parse(await readFile(path, "utf8"));
|
|
20
21
|
}
|
|
@@ -27,12 +28,9 @@ export async function loadLocalRunnerConfig(path) {
|
|
|
27
28
|
const configPath = resolve(path);
|
|
28
29
|
const base = dirname(configPath);
|
|
29
30
|
const config = localRunnerConfigSchema.parse(await loadJsonFile(configPath));
|
|
30
|
-
const configPathValue = (value
|
|
31
|
+
const configPathValue = (value) => {
|
|
31
32
|
if (!value)
|
|
32
33
|
return undefined;
|
|
33
|
-
if (preserveBundled && (value === "training/local-runner" || value.startsWith("training/local-runner/"))) {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
34
|
if (value === "~")
|
|
37
35
|
return homedir();
|
|
38
36
|
if (value.startsWith("~/"))
|
|
@@ -43,24 +41,18 @@ export async function loadLocalRunnerConfig(path) {
|
|
|
43
41
|
...config,
|
|
44
42
|
artifactRoot: configPathValue(config.artifactRoot),
|
|
45
43
|
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
44
|
paths: {
|
|
53
45
|
baseModel: configPathValue(config.paths.baseModel),
|
|
54
46
|
modelCache: configPathValue(config.paths.modelCache),
|
|
55
47
|
},
|
|
56
48
|
evaluation: {
|
|
57
49
|
...config.evaluation,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
generalRegression: config.evaluation.generalRegression
|
|
51
|
+
? {
|
|
52
|
+
...config.evaluation.generalRegression,
|
|
53
|
+
dataset: configPathValue(config.evaluation.generalRegression.dataset),
|
|
54
|
+
}
|
|
55
|
+
: undefined,
|
|
64
56
|
},
|
|
65
57
|
};
|
|
66
58
|
}
|
|
@@ -80,26 +72,149 @@ async function pathExists(path) {
|
|
|
80
72
|
return false;
|
|
81
73
|
}
|
|
82
74
|
}
|
|
83
|
-
function
|
|
75
|
+
function selectPrebuiltEvaluationSplit(dataset) {
|
|
84
76
|
if (!dataset)
|
|
85
77
|
throw new Error("dataset_prebuilt is required");
|
|
86
|
-
if (dataset.test)
|
|
87
|
-
return { path: stripFileUri(dataset.test), split: "prebuilt_test" };
|
|
88
78
|
if (dataset.validation)
|
|
89
|
-
return
|
|
90
|
-
|
|
79
|
+
return "prebuilt_validation";
|
|
80
|
+
if (dataset.test)
|
|
81
|
+
return "prebuilt_test";
|
|
82
|
+
throw new Error("dataset_prebuilt requires a distinct validation or test split");
|
|
91
83
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
function assertMatchingDatasetSystems(jsonl, expectedSystem, split) {
|
|
85
|
+
for (const [index, line] of jsonl.split(/\r?\n/).entries()) {
|
|
86
|
+
if (!line.trim())
|
|
87
|
+
continue;
|
|
88
|
+
const row = JSON.parse(line);
|
|
89
|
+
const actualSystem = row.messages.find((message) => message.role === "system")?.content ?? "";
|
|
90
|
+
if (actualSystem !== expectedSystem) {
|
|
91
|
+
throw new Error(`Prebuilt ${split} row ${index + 1} system message must match the behavior spec system message.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function addDryRunPlaceholders(request, dryRun) {
|
|
96
|
+
if (!dryRun || !request || typeof request !== "object" || Array.isArray(request))
|
|
97
|
+
return request;
|
|
98
|
+
const candidate = request;
|
|
99
|
+
if (candidate.dataset_prebuilt)
|
|
100
|
+
return request;
|
|
101
|
+
const snapshot = candidate.spec_snapshot;
|
|
102
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
|
|
103
|
+
return request;
|
|
104
|
+
const examples = snapshot.examples;
|
|
105
|
+
if (!Array.isArray(examples) || examples.length > 0)
|
|
106
|
+
return request;
|
|
107
|
+
return {
|
|
108
|
+
...candidate,
|
|
109
|
+
spec_snapshot: {
|
|
110
|
+
...snapshot,
|
|
111
|
+
examples: [
|
|
112
|
+
{ input: "Dry-run placeholder input A", output: "Dry-run placeholder output A" },
|
|
113
|
+
{ input: "Dry-run placeholder input B", output: "Dry-run placeholder output B" },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function validateDatasetInputs(request, config) {
|
|
119
|
+
const inputIdentity = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
|
|
120
|
+
const evalSampleSeed = config.evaluation.sampleSeed
|
|
121
|
+
?? deriveSampleSeed(request.behavior_spec_id);
|
|
122
|
+
if (!request.dataset_prebuilt) {
|
|
123
|
+
const inlineExamples = examplesFromSpec(request.spec_snapshot);
|
|
124
|
+
const seenInputs = new Map();
|
|
125
|
+
for (const [index, example] of inlineExamples.entries()) {
|
|
126
|
+
const identity = inputIdentity(example.input);
|
|
127
|
+
const previous = seenInputs.get(identity);
|
|
128
|
+
if (previous !== undefined) {
|
|
129
|
+
throw new Error(`Inline examples ${previous + 1} and ${index + 1} have duplicate inputs; `
|
|
130
|
+
+ "training and held-out evaluation require distinct prompts.");
|
|
131
|
+
}
|
|
132
|
+
seenInputs.set(identity, index);
|
|
133
|
+
}
|
|
134
|
+
if (!config.dryRun && inlineExamples.length < 2) {
|
|
135
|
+
throw new Error("A real local fine-tune requires at least 2 inline examples so training and evaluation use distinct data.");
|
|
136
|
+
}
|
|
137
|
+
const split = splitSpecExamples(inlineExamples, evalSampleSeed);
|
|
138
|
+
const hasHoldout = split.holdout.length > 0;
|
|
139
|
+
const trainingExamples = hasHoldout ? split.train : inlineExamples;
|
|
140
|
+
const evaluationExamples = hasHoldout ? split.holdout : inlineExamples;
|
|
141
|
+
return {
|
|
142
|
+
trainingJsonl: compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples }),
|
|
143
|
+
trainingExampleCount: trainingExamples.length,
|
|
144
|
+
evaluationExamples,
|
|
145
|
+
evalSplit: hasHoldout ? "spec_holdout" : "spec_examples",
|
|
146
|
+
};
|
|
95
147
|
}
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
const dataset = request.dataset_prebuilt;
|
|
149
|
+
const splitEntries = [
|
|
150
|
+
["training", dataset.training],
|
|
151
|
+
["validation", dataset.validation],
|
|
152
|
+
["test", dataset.test],
|
|
153
|
+
];
|
|
154
|
+
const normalized = new Map();
|
|
155
|
+
const expectedSystem = buildSystemMessage(request.spec_snapshot);
|
|
156
|
+
for (const [name, value] of splitEntries) {
|
|
157
|
+
if (!value)
|
|
158
|
+
continue;
|
|
159
|
+
const path = stripFileUri(value);
|
|
160
|
+
const jsonl = await normalizeChatJsonlForRelocation(path);
|
|
161
|
+
assertMatchingDatasetSystems(jsonl, expectedSystem, name);
|
|
162
|
+
normalized.set(name, {
|
|
163
|
+
jsonl,
|
|
164
|
+
examples: await examplesFromChatJsonl(path),
|
|
165
|
+
});
|
|
98
166
|
}
|
|
99
|
-
|
|
167
|
+
const evaluationSplit = selectPrebuiltEvaluationSplit(dataset);
|
|
168
|
+
const evaluationKey = evaluationSplit === "prebuilt_validation"
|
|
169
|
+
? "validation"
|
|
170
|
+
: "test";
|
|
171
|
+
const training = normalized.get("training");
|
|
172
|
+
const evaluationData = normalized.get(evaluationKey);
|
|
173
|
+
if (!training || !evaluationData) {
|
|
174
|
+
throw new Error("Unable to load the configured prebuilt training and evaluation datasets.");
|
|
175
|
+
}
|
|
176
|
+
const trainingInputs = new Set(training.examples.map((example) => inputIdentity(example.input)));
|
|
177
|
+
for (const splitName of ["validation", "test"]) {
|
|
178
|
+
const split = normalized.get(splitName);
|
|
179
|
+
if (!split)
|
|
180
|
+
continue;
|
|
181
|
+
const splitInputs = new Set();
|
|
182
|
+
for (const example of split.examples) {
|
|
183
|
+
const identity = inputIdentity(example.input);
|
|
184
|
+
if (splitInputs.has(identity)) {
|
|
185
|
+
throw new Error(`Prebuilt ${splitName} data contains duplicate inputs; `
|
|
186
|
+
+ "evaluation prompts must be unique so metrics and comparisons remain well-defined.");
|
|
187
|
+
}
|
|
188
|
+
splitInputs.add(identity);
|
|
189
|
+
}
|
|
190
|
+
const overlap = split.examples.filter((example) => trainingInputs.has(inputIdentity(example.input)));
|
|
191
|
+
if (overlap.length > 0) {
|
|
192
|
+
throw new Error(`Prebuilt training and ${splitName} data overlap on ${overlap.length} input(s); `
|
|
193
|
+
+ "evaluation examples must be held out from training.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
trainingJsonl: training.jsonl,
|
|
198
|
+
trainingExampleCount: training.examples.length,
|
|
199
|
+
evaluationExamples: evaluationData.examples,
|
|
200
|
+
evalSplit: evaluationSplit,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Parses the public contracts and fully validates every dataset before a run
|
|
205
|
+
* lock, store record, or artifact-directory claim is created.
|
|
206
|
+
*/
|
|
207
|
+
export async function validateLocalFineTuneInput(input) {
|
|
208
|
+
const config = localRunnerConfigSchema.parse(input.config);
|
|
209
|
+
const request = fineTuneRunRequestSchema.parse(addDryRunPlaceholders(input.request, config.dryRun));
|
|
210
|
+
await validateDatasetInputs(request, config);
|
|
211
|
+
if (config.evaluation.generalRegression) {
|
|
212
|
+
await evaluationSuiteFromChatJsonl(config.evaluation.generalRegression.dataset, config.evaluation.generalRegression.systemPrompt);
|
|
213
|
+
}
|
|
214
|
+
return { request, config };
|
|
100
215
|
}
|
|
101
216
|
function artifactPrefix(request) {
|
|
102
|
-
return
|
|
217
|
+
return defaultArtifactPrefix({
|
|
103
218
|
userId: request.user_id,
|
|
104
219
|
behaviorSpecId: request.behavior_spec_id,
|
|
105
220
|
runId: request.run_id,
|
|
@@ -134,33 +249,6 @@ async function packageVersion() {
|
|
|
134
249
|
return "unknown";
|
|
135
250
|
}
|
|
136
251
|
}
|
|
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
252
|
async function runtimeFingerprint() {
|
|
165
253
|
const bundledProject = resolve(packageRoot, "training/local-runner");
|
|
166
254
|
return hashJson({
|
|
@@ -179,14 +267,9 @@ function preparedSourceFingerprint(args) {
|
|
|
179
267
|
preparation_config: args.preparationConfig,
|
|
180
268
|
base_model_revision: args.baseModelRevision ?? null,
|
|
181
269
|
base_model_fingerprint: args.baseModelFingerprint ?? null,
|
|
182
|
-
parent_model_fingerprint: args.parentModelFingerprint ?? null,
|
|
183
270
|
dataset_fingerprints: args.datasetFingerprints,
|
|
184
271
|
});
|
|
185
272
|
}
|
|
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
273
|
async function hashFile(path) {
|
|
191
274
|
const hash = createHash("sha256");
|
|
192
275
|
for await (const chunk of createReadStream(path))
|
|
@@ -206,21 +289,6 @@ async function datasetFingerprints(request) {
|
|
|
206
289
|
continue;
|
|
207
290
|
const datasetPath = stripFileUri(value);
|
|
208
291
|
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
292
|
}
|
|
225
293
|
return fingerprints;
|
|
226
294
|
}
|
|
@@ -246,44 +314,6 @@ async function resolveBaseModelRevision(request, config) {
|
|
|
246
314
|
return undefined;
|
|
247
315
|
}
|
|
248
316
|
}
|
|
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
317
|
/** Hash a local base model while permitting Hugging Face snapshot file links. */
|
|
288
318
|
export async function fingerprintLocalBaseModel(uri) {
|
|
289
319
|
const root = localModelArtifactPath(uri);
|
|
@@ -331,8 +361,6 @@ function statusForProgressStage(stage) {
|
|
|
331
361
|
return "training";
|
|
332
362
|
if (stage === "preparing")
|
|
333
363
|
return "preparing";
|
|
334
|
-
if (stage === "scoring")
|
|
335
|
-
return "scoring";
|
|
336
364
|
if (stage === "reporting")
|
|
337
365
|
return "reporting";
|
|
338
366
|
return "training";
|
|
@@ -348,9 +376,6 @@ async function readStageMetadata(path) {
|
|
|
348
376
|
return null;
|
|
349
377
|
}
|
|
350
378
|
}
|
|
351
|
-
async function clearDependentStageArtifacts(artifacts) {
|
|
352
|
-
await cleanupStageArtifacts(artifacts, "prepare");
|
|
353
|
-
}
|
|
354
379
|
async function removePrefixedArtifacts(path) {
|
|
355
380
|
const directory = dirname(path);
|
|
356
381
|
const prefix = `${basename(path)}.`;
|
|
@@ -366,6 +391,8 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
366
391
|
await Promise.all([
|
|
367
392
|
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
368
393
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
394
|
+
removePrefixedArtifacts(artifacts.generalBaselineEvalJson),
|
|
395
|
+
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
369
396
|
rm(artifacts.trainingDir, { recursive: true, force: true }),
|
|
370
397
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
371
398
|
rm(resolve(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
@@ -377,6 +404,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
377
404
|
if (stage === "baseline") {
|
|
378
405
|
await Promise.all([
|
|
379
406
|
removePrefixedArtifacts(artifacts.baselineEvalJson),
|
|
407
|
+
removePrefixedArtifacts(artifacts.generalBaselineEvalJson),
|
|
380
408
|
removeReport(),
|
|
381
409
|
]);
|
|
382
410
|
return;
|
|
@@ -387,6 +415,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
387
415
|
removePrefixedArtifacts(artifacts.trainingReportJson),
|
|
388
416
|
rm(resolve(artifacts.runDir, "model.tar.gz"), { force: true }),
|
|
389
417
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
418
|
+
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
390
419
|
removeReport(),
|
|
391
420
|
]);
|
|
392
421
|
await prepareRunDirectories(artifacts);
|
|
@@ -395,6 +424,7 @@ async function cleanupStageArtifacts(artifacts, stage) {
|
|
|
395
424
|
if (stage === "candidate") {
|
|
396
425
|
await Promise.all([
|
|
397
426
|
removePrefixedArtifacts(artifacts.candidateEvalJson),
|
|
427
|
+
removePrefixedArtifacts(artifacts.generalCandidateEvalJson),
|
|
398
428
|
removeReport(),
|
|
399
429
|
]);
|
|
400
430
|
return;
|
|
@@ -489,54 +519,29 @@ async function acquireArtifactLock(artifacts) {
|
|
|
489
519
|
function isDryTraining(training) {
|
|
490
520
|
return training.metrics?.dry_run === true;
|
|
491
521
|
}
|
|
492
|
-
function stringMetadata(value, key) {
|
|
493
|
-
const entry = value?.[key];
|
|
494
|
-
return typeof entry === "string" && entry.length > 0 ? entry : undefined;
|
|
495
|
-
}
|
|
496
522
|
async function modelManifestContract(prepared, training) {
|
|
497
523
|
if (isDryTraining(training) || !training.model_artifact_uri)
|
|
498
524
|
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
|
-
});
|
|
525
|
+
const inspection = await assertUsableModelArtifact(training.model_artifact_uri);
|
|
504
526
|
const adapterWeights = inspection.adapter_weight_file_count > 0
|
|
505
527
|
&& 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.");
|
|
528
|
+
if (!adapterWeights || !inspection.has_adapter_config) {
|
|
529
|
+
throw new Error(`Local training must produce a PEFT adapter with adapter_model.safetensors or adapter_model.bin and `
|
|
530
|
+
+ `a non-empty adapter_config.json: ${inspection.path}`);
|
|
516
531
|
}
|
|
517
|
-
|
|
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");
|
|
532
|
+
const format = inspection.kind === "file" ? "tar.gz" : "huggingface-directory";
|
|
525
533
|
return {
|
|
526
534
|
artifact_kind: inspection.kind,
|
|
527
535
|
format,
|
|
528
|
-
framework:
|
|
536
|
+
framework: "transformers-peft",
|
|
529
537
|
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
|
|
538
|
+
base_model_revision: prepared.metadata.base_model_revision
|
|
533
539
|
?? prepared.request.hyperparameters.base_model_revision,
|
|
534
540
|
base_model_artifact_uri: training.base_model_artifact_uri,
|
|
535
541
|
base_model_fingerprint: prepared.metadata.base_model_fingerprint ?? undefined,
|
|
536
|
-
parent_model_artifact: prepared.request.hyperparameters.parent_model_artifact,
|
|
537
542
|
artifact_uri: training.model_artifact_uri,
|
|
538
543
|
artifact_root: inspection.path,
|
|
539
|
-
servable:
|
|
544
|
+
servable: true,
|
|
540
545
|
};
|
|
541
546
|
}
|
|
542
547
|
async function refreshArtifactManifest(prepared) {
|
|
@@ -557,37 +562,19 @@ function stageOutputPath(prepared, stage) {
|
|
|
557
562
|
function stageFingerprintPath(prepared, stage) {
|
|
558
563
|
return `${stageOutputPath(prepared, stage)}.stage.json`;
|
|
559
564
|
}
|
|
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
|
-
}
|
|
565
|
+
async function runnerFileFingerprints() {
|
|
566
|
+
const project = resolve(packageRoot, "training/local-runner");
|
|
567
|
+
const bundledSource = resolve(packageRoot, "training/local-runner/src");
|
|
568
|
+
const pythonSources = (await readdir(bundledSource, { withFileTypes: true }))
|
|
569
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".py"))
|
|
570
|
+
.map((entry) => resolve(bundledSource, entry.name));
|
|
571
|
+
const candidates = [...new Set([
|
|
572
|
+
...pythonSources,
|
|
573
|
+
resolve(project, "pyproject.toml"),
|
|
574
|
+
resolve(project, "uv.lock"),
|
|
575
|
+
])];
|
|
589
576
|
const fingerprints = {};
|
|
590
|
-
for (const path of
|
|
577
|
+
for (const path of candidates.sort())
|
|
591
578
|
fingerprints[path] = await hashFileIfPresent(path);
|
|
592
579
|
return fingerprints;
|
|
593
580
|
}
|
|
@@ -598,45 +585,26 @@ async function stageFingerprint(args) {
|
|
|
598
585
|
dry_run: args.config.dryRun,
|
|
599
586
|
};
|
|
600
587
|
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
|
-
});
|
|
588
|
+
const entrypointFiles = await runnerFileFingerprints();
|
|
612
589
|
return hashJson({
|
|
613
590
|
...common,
|
|
614
|
-
training: args.config.training,
|
|
615
591
|
paths: args.config.paths,
|
|
616
592
|
entrypoint_files: entrypointFiles,
|
|
617
593
|
});
|
|
618
594
|
}
|
|
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
|
-
});
|
|
595
|
+
const entrypointFiles = await runnerFileFingerprints();
|
|
631
596
|
const evaluation = {
|
|
632
|
-
|
|
633
|
-
? args.config.evaluation.baselineCommand
|
|
634
|
-
: args.config.evaluation.candidateCommand,
|
|
597
|
+
inference_protocol_version: INFERENCE_PROTOCOL_VERSION,
|
|
635
598
|
inference: args.config.evaluation.inference,
|
|
636
599
|
scoring: args.config.evaluation.scoring,
|
|
637
600
|
timeout_ms: args.config.evaluation.timeoutMs,
|
|
638
601
|
baseline_cache: args.config.evaluation.baselineCache,
|
|
639
|
-
|
|
602
|
+
general_regression: args.prepared.generalRegression
|
|
603
|
+
? {
|
|
604
|
+
dataset_sha256: args.prepared.generalRegression.datasetSha256,
|
|
605
|
+
system_prompt: args.prepared.generalRegression.system,
|
|
606
|
+
}
|
|
607
|
+
: null,
|
|
640
608
|
model_cache: args.config.paths.modelCache,
|
|
641
609
|
};
|
|
642
610
|
return hashJson({
|
|
@@ -739,64 +707,27 @@ async function ensureRunRecord(args) {
|
|
|
739
707
|
}
|
|
740
708
|
async function computePreparedRun(args) {
|
|
741
709
|
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
|
-
}
|
|
710
|
+
const evalSampleSeed = config.evaluation.sampleSeed
|
|
711
|
+
?? deriveSampleSeed(request.behavior_spec_id);
|
|
712
|
+
const dataset = await validateDatasetInputs(request, config);
|
|
713
|
+
const examples = dataset.evaluationExamples;
|
|
714
|
+
if (args.writeArtifacts) {
|
|
715
|
+
await writeFileAtomic(artifacts.trainingJsonl, `${dataset.trainingJsonl}\n`);
|
|
788
716
|
}
|
|
789
717
|
const system = buildSystemMessage(request.spec_snapshot);
|
|
790
718
|
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
719
|
const maxEvalExamples = config.evaluation.maxExamples ?? request.hyperparameters.max_eval_examples;
|
|
796
720
|
const evalExamplesUsed = Math.min(maxEvalExamples ?? examples.length, examples.length);
|
|
797
721
|
const fingerprints = await datasetFingerprints(request);
|
|
722
|
+
const generalRegressionConfig = config.evaluation.generalRegression;
|
|
723
|
+
const generalRegression = generalRegressionConfig
|
|
724
|
+
? {
|
|
725
|
+
datasetPath: generalRegressionConfig.dataset,
|
|
726
|
+
datasetSha256: await hashFile(generalRegressionConfig.dataset),
|
|
727
|
+
...await evaluationSuiteFromChatJsonl(generalRegressionConfig.dataset, generalRegressionConfig.systemPrompt),
|
|
728
|
+
}
|
|
729
|
+
: undefined;
|
|
798
730
|
const requestFingerprint = hashJson(request);
|
|
799
|
-
const effectiveConfigFingerprintValue = await effectiveConfigFingerprint(config);
|
|
800
731
|
const runtimeFingerprintValue = await runtimeFingerprint();
|
|
801
732
|
const baseModelRevision = await resolveBaseModelRevision(request, config);
|
|
802
733
|
const baseModelFingerprint = config.paths.baseModel
|
|
@@ -806,9 +737,7 @@ async function computePreparedRun(args) {
|
|
|
806
737
|
run_id: request.run_id,
|
|
807
738
|
behavior_spec_id: request.behavior_spec_id,
|
|
808
739
|
user_id: request.user_id,
|
|
809
|
-
training_method: request.training_method,
|
|
810
740
|
request_fingerprint: requestFingerprint,
|
|
811
|
-
effective_config_fingerprint: effectiveConfigFingerprintValue,
|
|
812
741
|
runtime_fingerprint: runtimeFingerprintValue,
|
|
813
742
|
source_fingerprint: preparedSourceFingerprint({
|
|
814
743
|
requestFingerprint,
|
|
@@ -818,19 +747,17 @@ async function computePreparedRun(args) {
|
|
|
818
747
|
base_model_path: config.paths.baseModel,
|
|
819
748
|
max_eval_examples: config.evaluation.maxExamples,
|
|
820
749
|
eval_sample_seed: config.evaluation.sampleSeed,
|
|
821
|
-
allow_prebuilt_training_eval: config.evaluation.allowPrebuiltTrainingEval,
|
|
822
750
|
},
|
|
823
751
|
baseModelRevision,
|
|
824
752
|
baseModelFingerprint,
|
|
825
|
-
parentModelFingerprint,
|
|
826
753
|
datasetFingerprints: fingerprints,
|
|
827
754
|
}),
|
|
828
|
-
eval_split: evalSplit,
|
|
755
|
+
eval_split: dataset.evalSplit,
|
|
829
756
|
eval_sample_seed: evalSampleSeed,
|
|
830
757
|
eval_examples_total: examples.length,
|
|
831
758
|
eval_examples_used: evalExamplesUsed,
|
|
832
759
|
max_eval_examples: maxEvalExamples ?? null,
|
|
833
|
-
training_example_count: trainingExampleCount,
|
|
760
|
+
training_example_count: dataset.trainingExampleCount,
|
|
834
761
|
dataset_prebuilt: Boolean(request.dataset_prebuilt),
|
|
835
762
|
dataset_format: request.dataset_prebuilt?.format ?? null,
|
|
836
763
|
dataset_fingerprints: fingerprints,
|
|
@@ -838,8 +765,6 @@ async function computePreparedRun(args) {
|
|
|
838
765
|
base_model_for_evaluation: baseModelForEvaluation,
|
|
839
766
|
base_model_revision: baseModelRevision ?? null,
|
|
840
767
|
base_model_fingerprint: baseModelFingerprint ?? null,
|
|
841
|
-
parent_model_artifact: parentModelArtifact ?? null,
|
|
842
|
-
parent_model_fingerprint: parentModelFingerprint ?? null,
|
|
843
768
|
system_prompt_sha256: createHash("sha256").update(system).digest("hex"),
|
|
844
769
|
prepared_at: new Date().toISOString(),
|
|
845
770
|
};
|
|
@@ -853,8 +778,22 @@ async function computePreparedRun(args) {
|
|
|
853
778
|
system,
|
|
854
779
|
baseModelForEvaluation,
|
|
855
780
|
maxEvalExamples,
|
|
781
|
+
generalRegression,
|
|
856
782
|
};
|
|
857
783
|
}
|
|
784
|
+
function generalRegressionEvaluationConfig(config) {
|
|
785
|
+
const { maxExamples: _maxExamples, sampleSeed: _sampleSeed, ...evaluation } = config.evaluation;
|
|
786
|
+
return { ...config, evaluation };
|
|
787
|
+
}
|
|
788
|
+
function generalRegressionRequiredPaths(prepared, kind) {
|
|
789
|
+
if (!prepared.generalRegression)
|
|
790
|
+
return [];
|
|
791
|
+
return [
|
|
792
|
+
kind === "baseline"
|
|
793
|
+
? prepared.artifacts.generalBaselineEvalJson
|
|
794
|
+
: prepared.artifacts.generalCandidateEvalJson,
|
|
795
|
+
];
|
|
796
|
+
}
|
|
858
797
|
async function prepareStage(args) {
|
|
859
798
|
const preparedExists = await pathExists(args.artifacts.stageMetadataJson)
|
|
860
799
|
&& await pathExists(args.artifacts.trainingJsonl);
|
|
@@ -865,7 +804,6 @@ async function prepareStage(args) {
|
|
|
865
804
|
? await readStageMetadata(args.artifacts.stageMetadataJson)
|
|
866
805
|
: null;
|
|
867
806
|
let canReuse = preparedExists
|
|
868
|
-
&& !args.force
|
|
869
807
|
&& existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
870
808
|
if (canReuse) {
|
|
871
809
|
canReuse = await verifyReusableArtifacts(args.artifacts, [
|
|
@@ -893,28 +831,18 @@ async function prepareStage(args) {
|
|
|
893
831
|
});
|
|
894
832
|
await throwIfCancelled(args.store, args.request);
|
|
895
833
|
await args.store.invalidateRunOutputs(args.request.run_id, { report: true, model: true });
|
|
896
|
-
await
|
|
834
|
+
await cleanupStageArtifacts(args.artifacts, "prepare");
|
|
897
835
|
const refreshed = await computePreparedRun({ ...args, writeArtifacts: true });
|
|
898
836
|
await refreshArtifactManifest(refreshed);
|
|
899
837
|
return refreshed;
|
|
900
838
|
}
|
|
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
839
|
async function runBaselineStage(args) {
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
840
|
+
if (await canReuseStageArtifact({
|
|
841
|
+
stage: "baseline",
|
|
842
|
+
prepared: args.prepared,
|
|
843
|
+
config: args.config,
|
|
844
|
+
additionalPaths: generalRegressionRequiredPaths(args.prepared, "baseline"),
|
|
845
|
+
})) {
|
|
918
846
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
919
847
|
await updateRun({
|
|
920
848
|
store: args.store,
|
|
@@ -943,19 +871,17 @@ async function runBaselineStage(args) {
|
|
|
943
871
|
examples: args.prepared.examples.length,
|
|
944
872
|
eval_examples_used: args.prepared.metadata.eval_examples_used,
|
|
945
873
|
eval_split: args.prepared.metadata.eval_split,
|
|
946
|
-
model_id: args.prepared.
|
|
947
|
-
parent_model_artifact: args.prepared.metadata.parent_model_artifact,
|
|
874
|
+
model_id: args.prepared.baseModelForEvaluation,
|
|
948
875
|
},
|
|
949
876
|
});
|
|
950
877
|
const report = await evaluateExamples({
|
|
951
878
|
kind: "baseline",
|
|
952
|
-
modelId: args.prepared.
|
|
879
|
+
modelId: args.prepared.baseModelForEvaluation,
|
|
953
880
|
baseModelId: args.prepared.baseModelForEvaluation,
|
|
954
881
|
baseModelRevision: args.config.paths.baseModel
|
|
955
882
|
? undefined
|
|
956
883
|
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
957
|
-
sourceFingerprint: args.prepared.metadata.
|
|
958
|
-
adapterPath: args.prepared.metadata.parent_model_artifact ?? undefined,
|
|
884
|
+
sourceFingerprint: args.prepared.metadata.base_model_fingerprint ?? undefined,
|
|
959
885
|
examples: args.prepared.examples,
|
|
960
886
|
system: args.prepared.system,
|
|
961
887
|
config: args.config,
|
|
@@ -966,18 +892,47 @@ async function runBaselineStage(args) {
|
|
|
966
892
|
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
967
893
|
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
968
894
|
});
|
|
895
|
+
if (args.prepared.generalRegression) {
|
|
896
|
+
await updateRun({
|
|
897
|
+
store: args.store,
|
|
898
|
+
reporter: args.reporter,
|
|
899
|
+
request: args.prepared.request,
|
|
900
|
+
status: "evaluating_baseline",
|
|
901
|
+
stage: "evaluating_baseline",
|
|
902
|
+
message: "Running baseline general regression evaluation.",
|
|
903
|
+
details: {
|
|
904
|
+
examples: args.prepared.generalRegression.examples.length,
|
|
905
|
+
dataset: args.prepared.generalRegression.datasetPath,
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
await evaluateExamples({
|
|
909
|
+
kind: "baseline",
|
|
910
|
+
modelId: args.prepared.baseModelForEvaluation,
|
|
911
|
+
baseModelId: args.prepared.baseModelForEvaluation,
|
|
912
|
+
baseModelRevision: args.config.paths.baseModel
|
|
913
|
+
? undefined
|
|
914
|
+
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
915
|
+
sourceFingerprint: args.prepared.metadata.base_model_fingerprint ?? undefined,
|
|
916
|
+
examples: args.prepared.generalRegression.examples,
|
|
917
|
+
system: args.prepared.generalRegression.system,
|
|
918
|
+
config: generalRegressionEvaluationConfig(args.config),
|
|
919
|
+
outputPath: args.prepared.artifacts.generalBaselineEvalJson,
|
|
920
|
+
reporter: args.runReporter,
|
|
921
|
+
evalSplit: "general_regression",
|
|
922
|
+
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
969
925
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
970
926
|
await writeStageFingerprint({ stage: "baseline", prepared: args.prepared, config: args.config });
|
|
971
927
|
return report;
|
|
972
928
|
}
|
|
973
929
|
async function runTrainStage(args) {
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
})) {
|
|
930
|
+
if (await canReuseStageArtifact({
|
|
931
|
+
stage: "train",
|
|
932
|
+
prepared: args.prepared,
|
|
933
|
+
config: args.config,
|
|
934
|
+
verifyModel: true,
|
|
935
|
+
})) {
|
|
981
936
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
982
937
|
await updateRun({
|
|
983
938
|
store: args.store,
|
|
@@ -1011,7 +966,7 @@ async function runTrainStage(args) {
|
|
|
1011
966
|
status: "training",
|
|
1012
967
|
stage: "training",
|
|
1013
968
|
message: args.config.dryRun ? "Recording dry-run training result." : "Launching local training process.",
|
|
1014
|
-
details: { training_backend:
|
|
969
|
+
details: { training_backend: "local-uv", dry_run: args.config.dryRun },
|
|
1015
970
|
});
|
|
1016
971
|
const training = await launchProcessTraining({
|
|
1017
972
|
request: args.prepared.request,
|
|
@@ -1043,27 +998,9 @@ async function runTrainStage(args) {
|
|
|
1043
998
|
}
|
|
1044
999
|
return training;
|
|
1045
1000
|
}
|
|
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
1001
|
async function runCandidateStage(args) {
|
|
1065
1002
|
let verifiedTraining;
|
|
1066
|
-
if (
|
|
1003
|
+
if (await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
1067
1004
|
const trainingCurrent = await canReuseStageArtifact({
|
|
1068
1005
|
stage: "train",
|
|
1069
1006
|
prepared: args.prepared,
|
|
@@ -1074,9 +1011,7 @@ async function runCandidateStage(args) {
|
|
|
1074
1011
|
verifiedTraining = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
1075
1012
|
}
|
|
1076
1013
|
}
|
|
1077
|
-
if (
|
|
1078
|
-
&& !args.modelArtifact
|
|
1079
|
-
&& verifiedTraining
|
|
1014
|
+
if (verifiedTraining
|
|
1080
1015
|
&& await canReuseStageArtifact({
|
|
1081
1016
|
stage: "candidate",
|
|
1082
1017
|
prepared: args.prepared,
|
|
@@ -1085,6 +1020,7 @@ async function runCandidateStage(args) {
|
|
|
1085
1020
|
additionalPaths: [
|
|
1086
1021
|
args.prepared.artifacts.trainingReportJson,
|
|
1087
1022
|
stageFingerprintPath(args.prepared, "train"),
|
|
1023
|
+
...generalRegressionRequiredPaths(args.prepared, "candidate"),
|
|
1088
1024
|
],
|
|
1089
1025
|
})) {
|
|
1090
1026
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
@@ -1110,33 +1046,16 @@ async function runCandidateStage(args) {
|
|
|
1110
1046
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
1111
1047
|
return report;
|
|
1112
1048
|
}
|
|
1113
|
-
if (!
|
|
1114
|
-
throw new Error("
|
|
1049
|
+
if (!verifiedTraining) {
|
|
1050
|
+
throw new Error("Candidate evaluation requires current verified training output.");
|
|
1115
1051
|
}
|
|
1116
1052
|
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
|
-
});
|
|
1053
|
+
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1121
1054
|
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
|
-
}
|
|
1055
|
+
const training = verifiedTraining;
|
|
1137
1056
|
const modelArtifact = training.model_artifact_uri;
|
|
1138
1057
|
if (!modelArtifact)
|
|
1139
|
-
throw new Error("candidate stage requires a model_artifact_uri in training-report.json
|
|
1058
|
+
throw new Error("candidate stage requires a model_artifact_uri in training-report.json.");
|
|
1140
1059
|
if (!isDryTraining(training)) {
|
|
1141
1060
|
await modelManifestContract(args.prepared, training);
|
|
1142
1061
|
await refreshArtifactManifest(args.prepared);
|
|
@@ -1174,6 +1093,36 @@ async function runCandidateStage(args) {
|
|
|
1174
1093
|
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
1175
1094
|
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
1176
1095
|
});
|
|
1096
|
+
if (args.prepared.generalRegression) {
|
|
1097
|
+
await updateRun({
|
|
1098
|
+
store: args.store,
|
|
1099
|
+
reporter: args.reporter,
|
|
1100
|
+
request: args.prepared.request,
|
|
1101
|
+
status: "evaluating_candidate",
|
|
1102
|
+
stage: "evaluating_candidate",
|
|
1103
|
+
message: "Running candidate general regression evaluation.",
|
|
1104
|
+
details: {
|
|
1105
|
+
examples: args.prepared.generalRegression.examples.length,
|
|
1106
|
+
dataset: args.prepared.generalRegression.datasetPath,
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
await evaluateExamples({
|
|
1110
|
+
kind: "candidate",
|
|
1111
|
+
modelId: modelArtifact,
|
|
1112
|
+
baseModelId: args.prepared.baseModelForEvaluation,
|
|
1113
|
+
baseModelRevision: args.config.paths.baseModel
|
|
1114
|
+
? undefined
|
|
1115
|
+
: args.prepared.metadata.base_model_revision ?? undefined,
|
|
1116
|
+
adapterPath: modelArtifact,
|
|
1117
|
+
examples: args.prepared.generalRegression.examples,
|
|
1118
|
+
system: args.prepared.generalRegression.system,
|
|
1119
|
+
config: generalRegressionEvaluationConfig(args.config),
|
|
1120
|
+
outputPath: args.prepared.artifacts.generalCandidateEvalJson,
|
|
1121
|
+
reporter: args.runReporter,
|
|
1122
|
+
evalSplit: "general_regression",
|
|
1123
|
+
shouldCancel: () => args.store.isCancellationRequested(args.prepared.request.run_id),
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1177
1126
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
1178
1127
|
await writeStageFingerprint({
|
|
1179
1128
|
stage: "candidate",
|
|
@@ -1183,82 +1132,20 @@ async function runCandidateStage(args) {
|
|
|
1183
1132
|
});
|
|
1184
1133
|
return report;
|
|
1185
1134
|
}
|
|
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
1135
|
async function runReportStage(args) {
|
|
1254
1136
|
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
1255
|
-
throw new Error("
|
|
1137
|
+
throw new Error("Run reporting requires baseline-eval.json.");
|
|
1256
1138
|
}
|
|
1257
1139
|
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
1258
|
-
throw new Error("
|
|
1140
|
+
throw new Error("Run reporting requires candidate-eval.json.");
|
|
1259
1141
|
}
|
|
1260
1142
|
if (!await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
1261
|
-
throw new Error("
|
|
1143
|
+
throw new Error("Run reporting requires training-report.json.");
|
|
1144
|
+
}
|
|
1145
|
+
if (args.prepared.generalRegression
|
|
1146
|
+
&& (!await pathExists(args.prepared.artifacts.generalBaselineEvalJson)
|
|
1147
|
+
|| !await pathExists(args.prepared.artifacts.generalCandidateEvalJson))) {
|
|
1148
|
+
throw new Error("Run reporting requires both general regression evaluations.");
|
|
1262
1149
|
}
|
|
1263
1150
|
const currentTraining = await canReuseStageArtifact({
|
|
1264
1151
|
stage: "train",
|
|
@@ -1270,12 +1157,18 @@ async function runReportStage(args) {
|
|
|
1270
1157
|
stage: "baseline",
|
|
1271
1158
|
prepared: args.prepared,
|
|
1272
1159
|
config: args.config,
|
|
1160
|
+
additionalPaths: generalRegressionRequiredPaths(args.prepared, "baseline"),
|
|
1273
1161
|
});
|
|
1274
1162
|
const currentCandidate = await canReuseStageArtifact({
|
|
1275
1163
|
stage: "candidate",
|
|
1276
1164
|
prepared: args.prepared,
|
|
1277
1165
|
config: args.config,
|
|
1278
1166
|
verifyModel: true,
|
|
1167
|
+
additionalPaths: [
|
|
1168
|
+
args.prepared.artifacts.trainingReportJson,
|
|
1169
|
+
stageFingerprintPath(args.prepared, "train"),
|
|
1170
|
+
...generalRegressionRequiredPaths(args.prepared, "candidate"),
|
|
1171
|
+
],
|
|
1279
1172
|
});
|
|
1280
1173
|
if (!currentTraining || !currentBaseline || !currentCandidate) {
|
|
1281
1174
|
throw new Error("report stage inputs are stale for the current request/config. Re-run baseline, train, and candidate as needed.");
|
|
@@ -1287,25 +1180,46 @@ async function runReportStage(args) {
|
|
|
1287
1180
|
stageFingerprintPath(args.prepared, "candidate"),
|
|
1288
1181
|
args.prepared.artifacts.trainingReportJson,
|
|
1289
1182
|
stageFingerprintPath(args.prepared, "train"),
|
|
1183
|
+
...generalRegressionRequiredPaths(args.prepared, "baseline"),
|
|
1184
|
+
...generalRegressionRequiredPaths(args.prepared, "candidate"),
|
|
1290
1185
|
], { verifyModel: true });
|
|
1291
1186
|
await throwIfCancelled(args.store, args.prepared.request);
|
|
1292
1187
|
await args.store.invalidateRunOutputs(args.prepared.request.run_id, { report: true });
|
|
1293
1188
|
await cleanupStageArtifacts(args.prepared.artifacts, "report");
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1189
|
+
await updateRun({
|
|
1190
|
+
store: args.store,
|
|
1191
|
+
reporter: args.reporter,
|
|
1192
|
+
request: args.prepared.request,
|
|
1193
|
+
status: "reporting",
|
|
1194
|
+
stage: "reporting",
|
|
1195
|
+
message: "Writing run report.",
|
|
1196
|
+
details: { report_path: args.prepared.artifacts.runReportJson },
|
|
1197
|
+
});
|
|
1305
1198
|
const baseline = evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
|
|
1306
1199
|
const candidate = evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
|
|
1307
1200
|
const training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
1308
1201
|
const comparison = compareEvalReports(baseline, candidate);
|
|
1202
|
+
let generalRegression;
|
|
1203
|
+
if (args.prepared.generalRegression) {
|
|
1204
|
+
const generalBaseline = evalReportSchema.parse(await readJson(args.prepared.artifacts.generalBaselineEvalJson));
|
|
1205
|
+
const generalCandidate = evalReportSchema.parse(await readJson(args.prepared.artifacts.generalCandidateEvalJson));
|
|
1206
|
+
const generalComparison = compareEvalReports(generalBaseline, generalCandidate);
|
|
1207
|
+
const policy = args.config.evaluation.generalRegression;
|
|
1208
|
+
const gate = evaluateGeneralRegressionGate(generalComparison, policy);
|
|
1209
|
+
generalRegression = {
|
|
1210
|
+
dataset_uri: fileUri(args.prepared.generalRegression.datasetPath),
|
|
1211
|
+
dataset_sha256: args.prepared.generalRegression.datasetSha256,
|
|
1212
|
+
baseline: generalBaseline,
|
|
1213
|
+
candidate: generalCandidate,
|
|
1214
|
+
comparison: generalComparison,
|
|
1215
|
+
policy: {
|
|
1216
|
+
max_score_drop: policy.maxScoreDrop,
|
|
1217
|
+
max_pass_rate_drop: policy.maxPassRateDrop,
|
|
1218
|
+
},
|
|
1219
|
+
passed: gate.passed,
|
|
1220
|
+
failures: gate.failures,
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1309
1223
|
const completedAt = new Date().toISOString();
|
|
1310
1224
|
const duration = elapsed(args.startedPerf);
|
|
1311
1225
|
const report = runReportSchema.parse({
|
|
@@ -1319,18 +1233,23 @@ async function runReportStage(args) {
|
|
|
1319
1233
|
baseline,
|
|
1320
1234
|
candidate,
|
|
1321
1235
|
comparison,
|
|
1236
|
+
general_regression: generalRegression,
|
|
1322
1237
|
training,
|
|
1323
1238
|
artifact_uris: {
|
|
1324
1239
|
dataset: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
1325
1240
|
baseline_eval: fileUri(args.prepared.artifacts.baselineEvalJson),
|
|
1326
1241
|
candidate_eval: fileUri(args.prepared.artifacts.candidateEvalJson),
|
|
1242
|
+
general_baseline_eval: generalRegression
|
|
1243
|
+
? fileUri(args.prepared.artifacts.generalBaselineEvalJson)
|
|
1244
|
+
: undefined,
|
|
1245
|
+
general_candidate_eval: generalRegression
|
|
1246
|
+
? fileUri(args.prepared.artifacts.generalCandidateEvalJson)
|
|
1247
|
+
: undefined,
|
|
1327
1248
|
report: fileUri(args.prepared.artifacts.runReportJson),
|
|
1328
1249
|
},
|
|
1329
1250
|
run_metadata: {
|
|
1330
1251
|
base_model: args.prepared.request.spec_snapshot.base_model,
|
|
1331
1252
|
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
1253
|
dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
|
|
1335
1254
|
dataset_format: args.prepared.metadata.dataset_format,
|
|
1336
1255
|
dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
@@ -1362,15 +1281,20 @@ async function runReportStage(args) {
|
|
|
1362
1281
|
report_path: args.prepared.artifacts.runReportJson,
|
|
1363
1282
|
...(!isDryTraining(training) ? { model_id: `local-${args.prepared.request.run_id}` } : {}),
|
|
1364
1283
|
avg_score_delta: comparison.avg_score_delta,
|
|
1284
|
+
general_regression_passed: generalRegression?.passed,
|
|
1365
1285
|
elapsed_seconds: duration.seconds,
|
|
1366
1286
|
},
|
|
1367
1287
|
});
|
|
1368
1288
|
return report;
|
|
1369
1289
|
}
|
|
1370
|
-
export async function
|
|
1290
|
+
export async function runLocalFineTune(input) {
|
|
1291
|
+
const validated = await validateLocalFineTuneInput({
|
|
1292
|
+
request: input.request,
|
|
1293
|
+
config: input.config,
|
|
1294
|
+
});
|
|
1295
|
+
input = { ...input, ...validated };
|
|
1371
1296
|
const startedPerf = performance.now();
|
|
1372
1297
|
const startedAt = new Date().toISOString();
|
|
1373
|
-
const stage = input.stage ?? "all";
|
|
1374
1298
|
const prefix = artifactPrefix(input.request);
|
|
1375
1299
|
const artifacts = resolveRunArtifacts({ artifactRoot: input.config.artifactRoot, prefix });
|
|
1376
1300
|
const store = createLocalStore(input.config.storeRoot);
|
|
@@ -1389,110 +1313,51 @@ export async function runLocalFineTuneStage(input) {
|
|
|
1389
1313
|
let prepared;
|
|
1390
1314
|
try {
|
|
1391
1315
|
await throwIfCancelled(store, input.request);
|
|
1392
|
-
prepared = await
|
|
1316
|
+
prepared = await prepareStage({
|
|
1393
1317
|
request: input.request,
|
|
1394
1318
|
config: input.config,
|
|
1395
1319
|
artifacts,
|
|
1396
1320
|
store,
|
|
1397
1321
|
reporter: input.reporter,
|
|
1398
|
-
forcePrepare: Boolean(input.force && (stage === "prepare" || stage === "all")),
|
|
1399
1322
|
});
|
|
1400
1323
|
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
|
-
|
|
1324
|
+
await runBaselineStage({
|
|
1325
|
+
prepared,
|
|
1326
|
+
config: input.config,
|
|
1327
|
+
store,
|
|
1328
|
+
reporter: input.reporter,
|
|
1329
|
+
runReporter,
|
|
1330
|
+
});
|
|
1331
|
+
await refreshArtifactManifest(prepared);
|
|
1332
|
+
await runTrainStage({
|
|
1333
|
+
prepared,
|
|
1334
|
+
config: input.config,
|
|
1335
|
+
store,
|
|
1336
|
+
reporter: input.reporter,
|
|
1337
|
+
runReporter,
|
|
1338
|
+
});
|
|
1339
|
+
await runCandidateStage({
|
|
1340
|
+
prepared,
|
|
1341
|
+
config: input.config,
|
|
1342
|
+
store,
|
|
1343
|
+
reporter: input.reporter,
|
|
1344
|
+
runReporter,
|
|
1345
|
+
});
|
|
1346
|
+
await refreshArtifactManifest(prepared);
|
|
1347
|
+
const report = await runReportStage({
|
|
1348
|
+
prepared,
|
|
1349
|
+
config: input.config,
|
|
1350
|
+
store,
|
|
1351
|
+
reporter: input.reporter,
|
|
1352
|
+
startedAt,
|
|
1353
|
+
startedPerf,
|
|
1354
|
+
});
|
|
1355
|
+
return {
|
|
1356
|
+
request: input.request,
|
|
1357
|
+
report,
|
|
1358
|
+
reportPath: artifacts.runReportJson,
|
|
1359
|
+
artifactDir: artifacts.runDir,
|
|
1421
1360
|
};
|
|
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
1361
|
}
|
|
1497
1362
|
catch (error) {
|
|
1498
1363
|
const cancelled = error instanceof ProcessCancelledError
|
|
@@ -1524,39 +1389,4 @@ export async function runLocalFineTuneStage(input) {
|
|
|
1524
1389
|
await releaseRunLock();
|
|
1525
1390
|
}
|
|
1526
1391
|
}
|
|
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
1392
|
//# sourceMappingURL=orchestrator.js.map
|