@tuned-tensor/local 0.2.2 → 0.2.4
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 +43 -0
- package/README.md +28 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +2 -0
- package/dist/artifacts.js.map +1 -1
- package/dist/contracts.d.ts +47 -0
- package/dist/contracts.js +47 -5
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +7 -0
- package/dist/evaluation.js +173 -122
- package/dist/evaluation.js.map +1 -1
- package/dist/index.js +69 -18
- package/dist/index.js.map +1 -1
- package/dist/local-project.js +2 -1
- package/dist/local-project.js.map +1 -1
- package/dist/orchestrator.d.ts +24 -0
- package/dist/orchestrator.js +623 -189
- package/dist/orchestrator.js.map +1 -1
- package/dist/process-training.js +26 -2
- package/dist/process-training.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +6 -5
- package/docs/spark.md +14 -6
- package/examples/dpo-preferences.jsonl +2 -0
- package/examples/dpo-run-request.json +34 -0
- package/examples/local-runner.json +1 -2
- package/package.json +1 -1
- package/training/{sft-local → local-runner}/pyproject.toml +1 -1
- package/training/local-runner/src/train_dpo.py +232 -0
- /package/training/{sft-local → local-runner}/src/evaluate.py +0 -0
- /package/training/{sft-local → local-runner}/src/train.py +0 -0
package/dist/orchestrator.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { performance } from "node:perf_hooks";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { defaultArtifactPrefix, fileUri, prepareRunDirectories, resolveRunArtifacts, writeJson, } from "./artifacts.js";
|
|
5
|
-
import { fineTuneRunRequestSchema, localRunnerConfigSchema, runReportSchema, } from "./contracts.js";
|
|
5
|
+
import { defaultArtifactPrefix, fileUri, prepareRunDirectories, readJson, resolveRunArtifacts, writeJson, } from "./artifacts.js";
|
|
6
|
+
import { evalReportSchema, fineTuneRunRequestSchema, localRunnerConfigSchema, runReportSchema, trainingReportSchema, } from "./contracts.js";
|
|
6
7
|
import { buildSystemMessage, compileSpecToJsonl, examplesFromChatJsonl, examplesFromSpec } from "./dataset.js";
|
|
7
|
-
import { compareEvalReports, deriveSampleSeed, evaluateExamples, splitSpecExamples } from "./evaluation.js";
|
|
8
|
+
import { compareEvalReports, deriveSampleSeed, evaluateExamples, rescoreEvalReport, splitSpecExamples } from "./evaluation.js";
|
|
8
9
|
import { launchProcessTraining } from "./process-training.js";
|
|
9
10
|
import { createLocalStore } from "./store.js";
|
|
10
11
|
export async function loadJsonFile(path) {
|
|
@@ -25,6 +26,15 @@ function elapsed(started) {
|
|
|
25
26
|
function stripFileUri(path) {
|
|
26
27
|
return path.replace(/^file:\/\//, "");
|
|
27
28
|
}
|
|
29
|
+
async function pathExists(path) {
|
|
30
|
+
try {
|
|
31
|
+
await stat(path);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
28
38
|
function selectPrebuiltEvaluation(dataset) {
|
|
29
39
|
if (!dataset)
|
|
30
40
|
throw new Error("dataset_prebuilt is required");
|
|
@@ -34,223 +44,613 @@ function selectPrebuiltEvaluation(dataset) {
|
|
|
34
44
|
return { path: stripFileUri(dataset.validation), split: "prebuilt_validation" };
|
|
35
45
|
return { path: stripFileUri(dataset.training), split: "prebuilt_training" };
|
|
36
46
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
function selectDpoEvaluation(request) {
|
|
48
|
+
if (request.dataset_prebuilt?.test) {
|
|
49
|
+
return { path: stripFileUri(request.dataset_prebuilt.test), split: "prebuilt_test" };
|
|
50
|
+
}
|
|
51
|
+
if (request.dataset_prebuilt?.validation) {
|
|
52
|
+
return { path: stripFileUri(request.dataset_prebuilt.validation), split: "prebuilt_validation" };
|
|
53
|
+
}
|
|
54
|
+
return { split: "spec_examples" };
|
|
55
|
+
}
|
|
56
|
+
function artifactPrefix(request) {
|
|
57
|
+
return request.artifacts?.prefix ?? defaultArtifactPrefix({
|
|
42
58
|
userId: request.user_id,
|
|
43
59
|
behaviorSpecId: request.behavior_spec_id,
|
|
44
60
|
runId: request.run_id,
|
|
45
61
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
}
|
|
63
|
+
function hashJson(value) {
|
|
64
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
65
|
+
}
|
|
66
|
+
function preparedSourceFingerprint(args) {
|
|
67
|
+
return hashJson({
|
|
68
|
+
training_method: args.request.training_method,
|
|
69
|
+
spec_snapshot: args.request.spec_snapshot,
|
|
70
|
+
dataset_prebuilt: args.request.dataset_prebuilt ?? null,
|
|
71
|
+
dataset_fingerprints: args.datasetFingerprints,
|
|
72
|
+
base_model_for_evaluation: args.baseModelForEvaluation,
|
|
73
|
+
eval_sample_seed: args.evalSampleSeed,
|
|
74
|
+
max_eval_examples: args.maxEvalExamples ?? null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function countJsonlRows(path) {
|
|
78
|
+
const text = await readFile(path, "utf8");
|
|
79
|
+
return text.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
80
|
+
}
|
|
81
|
+
async function hashFile(path) {
|
|
82
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
83
|
+
}
|
|
84
|
+
async function datasetFingerprints(request) {
|
|
85
|
+
const dataset = request.dataset_prebuilt;
|
|
86
|
+
if (!dataset)
|
|
87
|
+
return {};
|
|
88
|
+
const entries = [
|
|
89
|
+
["training", dataset.training],
|
|
90
|
+
["validation", dataset.validation],
|
|
91
|
+
["test", dataset.test],
|
|
92
|
+
];
|
|
93
|
+
const fingerprints = {};
|
|
94
|
+
for (const [key, value] of entries) {
|
|
95
|
+
if (value)
|
|
96
|
+
fingerprints[key] = await hashFile(stripFileUri(value));
|
|
97
|
+
}
|
|
98
|
+
return fingerprints;
|
|
99
|
+
}
|
|
100
|
+
function statusForProgressStage(stage) {
|
|
101
|
+
if (stage === "evaluating_baseline")
|
|
102
|
+
return "evaluating_baseline";
|
|
103
|
+
if (stage === "evaluating_candidate")
|
|
104
|
+
return "evaluating_candidate";
|
|
105
|
+
if (stage === "training")
|
|
106
|
+
return "training";
|
|
107
|
+
if (stage === "preparing")
|
|
108
|
+
return "preparing";
|
|
109
|
+
if (stage === "scoring")
|
|
110
|
+
return "scoring";
|
|
111
|
+
if (stage === "reporting")
|
|
112
|
+
return "reporting";
|
|
113
|
+
return "training";
|
|
114
|
+
}
|
|
115
|
+
async function readStageMetadata(path) {
|
|
51
116
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
117
|
+
const metadata = await readJson(path);
|
|
118
|
+
return typeof metadata.source_fingerprint === "string"
|
|
119
|
+
? metadata
|
|
120
|
+
: null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function clearDependentStageArtifacts(artifacts) {
|
|
127
|
+
await Promise.all([
|
|
128
|
+
rm(artifacts.baselineEvalJson, { force: true }),
|
|
129
|
+
rm(artifacts.candidateEvalJson, { force: true }),
|
|
130
|
+
rm(artifacts.trainingReportJson, { force: true }),
|
|
131
|
+
rm(artifacts.runReportJson, { force: true }),
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
function createStoreReporter(input) {
|
|
135
|
+
return {
|
|
136
|
+
verbose: input.reporter?.verbose,
|
|
137
|
+
async onEvent(event) {
|
|
138
|
+
await input.store.updateRun({
|
|
139
|
+
runId: input.request.run_id,
|
|
140
|
+
status: statusForProgressStage(event.stage),
|
|
141
|
+
stage: event.stage,
|
|
142
|
+
message: event.message,
|
|
143
|
+
details: event.details,
|
|
59
144
|
});
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
await input.reporter?.onEvent?.(event);
|
|
146
|
+
},
|
|
147
|
+
async onLog(log) {
|
|
148
|
+
await input.reporter?.onLog?.(log);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function updateRun(input) {
|
|
153
|
+
const state = await input.store.updateRun({
|
|
154
|
+
runId: input.request.run_id,
|
|
155
|
+
status: input.status,
|
|
156
|
+
stage: input.stage,
|
|
157
|
+
message: input.message,
|
|
158
|
+
details: input.details,
|
|
159
|
+
});
|
|
160
|
+
await input.reporter?.onEvent?.({
|
|
161
|
+
stage: input.stage,
|
|
162
|
+
status: input.status,
|
|
163
|
+
message: input.message,
|
|
164
|
+
details: input.details,
|
|
165
|
+
});
|
|
166
|
+
return state;
|
|
167
|
+
}
|
|
168
|
+
async function ensureRunRecord(args) {
|
|
169
|
+
await prepareRunDirectories(args.artifacts);
|
|
170
|
+
await writeJson(resolve(args.artifacts.runDir, "request.json"), args.request);
|
|
171
|
+
try {
|
|
172
|
+
await args.store.getRun(args.request.run_id);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
await args.store.startRun({ request: args.request, artifactDir: args.artifacts.runDir });
|
|
176
|
+
await args.reporter?.onEvent?.({
|
|
90
177
|
stage: "queued",
|
|
91
178
|
status: "queued",
|
|
92
179
|
message: "Run queued.",
|
|
93
|
-
details: { run_id: request.run_id, artifact_dir: artifacts.runDir },
|
|
94
|
-
});
|
|
95
|
-
await updateRun({
|
|
96
|
-
status: "preparing",
|
|
97
|
-
stage: "preparing",
|
|
98
|
-
message: "Preparing local run artifacts.",
|
|
99
|
-
details: { artifact_dir: artifacts.runDir },
|
|
180
|
+
details: { run_id: args.request.run_id, artifact_dir: args.artifacts.runDir },
|
|
100
181
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw new Error("dataset_prebuilt has no test or validation split, so evaluation would run on the training data and "
|
|
115
|
-
+ "overstate improvement. Provide dataset_prebuilt.test or dataset_prebuilt.validation, or set "
|
|
116
|
-
+ "evaluation.allowPrebuiltTrainingEval=true to evaluate on the training split anyway.");
|
|
117
|
-
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function computePreparedRun(args) {
|
|
185
|
+
const { request, config, artifacts } = args;
|
|
186
|
+
const evalSampleSeed = config.evaluation.sampleSeed ?? deriveSampleSeed(request.run_id);
|
|
187
|
+
let examples = examplesFromSpec(request.spec_snapshot);
|
|
188
|
+
let evalSplit = "spec_examples";
|
|
189
|
+
let trainingExampleCount = examples.length;
|
|
190
|
+
if (request.training_method === "dpo") {
|
|
191
|
+
if (!request.dataset_prebuilt)
|
|
192
|
+
throw new Error("DPO training requires dataset_prebuilt.");
|
|
193
|
+
const trainingPath = stripFileUri(request.dataset_prebuilt.training);
|
|
194
|
+
if (args.writeArtifacts)
|
|
118
195
|
await copyFile(trainingPath, artifacts.trainingJsonl);
|
|
119
|
-
|
|
120
|
-
|
|
196
|
+
trainingExampleCount = await countJsonlRows(trainingPath);
|
|
197
|
+
const evaluation = selectDpoEvaluation(request);
|
|
198
|
+
evalSplit = evaluation.split;
|
|
199
|
+
examples = evaluation.path
|
|
200
|
+
? await examplesFromChatJsonl(evaluation.path)
|
|
201
|
+
: examplesFromSpec(request.spec_snapshot);
|
|
202
|
+
}
|
|
203
|
+
else if (request.dataset_prebuilt) {
|
|
204
|
+
const trainingPath = stripFileUri(request.dataset_prebuilt.training);
|
|
205
|
+
const evaluation = selectPrebuiltEvaluation(request.dataset_prebuilt);
|
|
206
|
+
evalSplit = evaluation.split;
|
|
207
|
+
if (evalSplit === "prebuilt_training" && !config.dryRun && !config.evaluation.allowPrebuiltTrainingEval) {
|
|
208
|
+
throw new Error("dataset_prebuilt has no test or validation split, so evaluation would run on the training data and "
|
|
209
|
+
+ "overstate improvement. Provide dataset_prebuilt.test or dataset_prebuilt.validation, or set "
|
|
210
|
+
+ "evaluation.allowPrebuiltTrainingEval=true to evaluate on the training split anyway.");
|
|
121
211
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
if (args.writeArtifacts)
|
|
213
|
+
await copyFile(trainingPath, artifacts.trainingJsonl);
|
|
214
|
+
examples = await examplesFromChatJsonl(evaluation.path);
|
|
215
|
+
trainingExampleCount = null;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const split = splitSpecExamples(request.spec_snapshot.examples, evalSampleSeed);
|
|
219
|
+
let trainingExamples = request.spec_snapshot.examples;
|
|
220
|
+
if (split.holdout.length > 0) {
|
|
221
|
+
trainingExamples = split.train;
|
|
222
|
+
examples = split.holdout;
|
|
223
|
+
evalSplit = "spec_holdout";
|
|
224
|
+
}
|
|
225
|
+
trainingExampleCount = trainingExamples.length;
|
|
226
|
+
if (args.writeArtifacts) {
|
|
133
227
|
const jsonl = compileSpecToJsonl({ ...request.spec_snapshot, examples: trainingExamples });
|
|
134
228
|
await writeFile(artifacts.trainingJsonl, `${jsonl}\n`, "utf8");
|
|
135
229
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
230
|
+
}
|
|
231
|
+
const system = buildSystemMessage(request.spec_snapshot);
|
|
232
|
+
const baseModelForEvaluation = config.paths.baseModel ?? request.spec_snapshot.base_model;
|
|
233
|
+
const maxEvalExamples = config.evaluation.maxExamples ?? request.hyperparameters.max_eval_examples;
|
|
234
|
+
const evalExamplesUsed = Math.min(maxEvalExamples ?? examples.length, examples.length);
|
|
235
|
+
const fingerprints = await datasetFingerprints(request);
|
|
236
|
+
const metadata = {
|
|
237
|
+
run_id: request.run_id,
|
|
238
|
+
behavior_spec_id: request.behavior_spec_id,
|
|
239
|
+
user_id: request.user_id,
|
|
240
|
+
training_method: request.training_method,
|
|
241
|
+
source_fingerprint: preparedSourceFingerprint({
|
|
242
|
+
request,
|
|
243
|
+
baseModelForEvaluation,
|
|
244
|
+
evalSampleSeed,
|
|
245
|
+
maxEvalExamples,
|
|
246
|
+
datasetFingerprints: fingerprints,
|
|
247
|
+
}),
|
|
248
|
+
eval_split: evalSplit,
|
|
249
|
+
eval_sample_seed: evalSampleSeed,
|
|
250
|
+
eval_examples_total: examples.length,
|
|
251
|
+
eval_examples_used: evalExamplesUsed,
|
|
252
|
+
max_eval_examples: maxEvalExamples ?? null,
|
|
253
|
+
training_example_count: trainingExampleCount,
|
|
254
|
+
dataset_prebuilt: Boolean(request.dataset_prebuilt),
|
|
255
|
+
dataset_format: request.dataset_prebuilt?.format ?? null,
|
|
256
|
+
dataset_fingerprints: fingerprints,
|
|
257
|
+
dataset_uri: fileUri(artifacts.trainingJsonl),
|
|
258
|
+
base_model_for_evaluation: baseModelForEvaluation,
|
|
259
|
+
system_prompt_sha256: createHash("sha256").update(system).digest("hex"),
|
|
260
|
+
prepared_at: new Date().toISOString(),
|
|
261
|
+
};
|
|
262
|
+
if (args.writeArtifacts)
|
|
263
|
+
await writeJson(artifacts.stageMetadataJson, metadata);
|
|
264
|
+
return {
|
|
265
|
+
request,
|
|
266
|
+
artifacts,
|
|
267
|
+
metadata,
|
|
268
|
+
examples,
|
|
269
|
+
system,
|
|
270
|
+
baseModelForEvaluation,
|
|
271
|
+
maxEvalExamples,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async function prepareStage(args) {
|
|
275
|
+
const preparedExists = await pathExists(args.artifacts.stageMetadataJson)
|
|
276
|
+
&& await pathExists(args.artifacts.trainingJsonl);
|
|
277
|
+
const prepared = await computePreparedRun({ ...args, writeArtifacts: false });
|
|
278
|
+
const existingMetadata = preparedExists
|
|
279
|
+
? await readStageMetadata(args.artifacts.stageMetadataJson)
|
|
280
|
+
: null;
|
|
281
|
+
const canReuse = preparedExists
|
|
282
|
+
&& !args.force
|
|
283
|
+
&& existingMetadata?.source_fingerprint === prepared.metadata.source_fingerprint;
|
|
284
|
+
if (canReuse) {
|
|
285
|
+
await updateRun({
|
|
286
|
+
...args,
|
|
287
|
+
status: "preparing",
|
|
288
|
+
stage: "preparing",
|
|
289
|
+
message: "Reusing prepared local run artifacts.",
|
|
290
|
+
details: { artifact_dir: args.artifacts.runDir },
|
|
291
|
+
});
|
|
292
|
+
return prepared;
|
|
293
|
+
}
|
|
294
|
+
await updateRun({
|
|
295
|
+
...args,
|
|
296
|
+
status: "preparing",
|
|
297
|
+
stage: "preparing",
|
|
298
|
+
message: "Preparing local run artifacts.",
|
|
299
|
+
details: { artifact_dir: args.artifacts.runDir },
|
|
300
|
+
});
|
|
301
|
+
await clearDependentStageArtifacts(args.artifacts);
|
|
302
|
+
return computePreparedRun({ ...args, writeArtifacts: true });
|
|
303
|
+
}
|
|
304
|
+
async function ensurePrepared(args) {
|
|
305
|
+
return prepareStage({
|
|
306
|
+
request: args.request,
|
|
307
|
+
config: args.config,
|
|
308
|
+
artifacts: args.artifacts,
|
|
309
|
+
store: args.store,
|
|
310
|
+
reporter: args.reporter,
|
|
311
|
+
force: args.forcePrepare,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function runBaselineStage(args) {
|
|
315
|
+
if (!args.force && await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
141
316
|
await updateRun({
|
|
317
|
+
store: args.store,
|
|
318
|
+
reporter: args.reporter,
|
|
319
|
+
request: args.prepared.request,
|
|
142
320
|
status: "evaluating_baseline",
|
|
143
321
|
stage: "evaluating_baseline",
|
|
144
|
-
message: "
|
|
145
|
-
details: {
|
|
146
|
-
examples: examples.length,
|
|
147
|
-
eval_examples_used: Math.min(maxEvalExamples ?? examples.length, examples.length),
|
|
148
|
-
eval_split: evalSplit,
|
|
149
|
-
model_id: baseModelForEvaluation,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
const baseline = await evaluateExamples({
|
|
153
|
-
kind: "baseline",
|
|
154
|
-
modelId: baseModelForEvaluation,
|
|
155
|
-
baseModelId: baseModelForEvaluation,
|
|
156
|
-
examples,
|
|
157
|
-
system,
|
|
158
|
-
config,
|
|
159
|
-
outputPath: artifacts.baselineEvalJson,
|
|
160
|
-
reporter: runReporter,
|
|
161
|
-
maxExamples: maxEvalExamples,
|
|
162
|
-
evalSplit,
|
|
163
|
-
sampleSeed: evalSampleSeed,
|
|
322
|
+
message: "Reusing existing baseline evaluation.",
|
|
323
|
+
details: { path: args.prepared.artifacts.baselineEvalJson },
|
|
164
324
|
});
|
|
325
|
+
return evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
|
|
326
|
+
}
|
|
327
|
+
await updateRun({
|
|
328
|
+
store: args.store,
|
|
329
|
+
reporter: args.reporter,
|
|
330
|
+
request: args.prepared.request,
|
|
331
|
+
status: "evaluating_baseline",
|
|
332
|
+
stage: "evaluating_baseline",
|
|
333
|
+
message: "Running baseline evaluation.",
|
|
334
|
+
details: {
|
|
335
|
+
examples: args.prepared.examples.length,
|
|
336
|
+
eval_examples_used: args.prepared.metadata.eval_examples_used,
|
|
337
|
+
eval_split: args.prepared.metadata.eval_split,
|
|
338
|
+
model_id: args.prepared.baseModelForEvaluation,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return evaluateExamples({
|
|
342
|
+
kind: "baseline",
|
|
343
|
+
modelId: args.prepared.baseModelForEvaluation,
|
|
344
|
+
baseModelId: args.prepared.baseModelForEvaluation,
|
|
345
|
+
examples: args.prepared.examples,
|
|
346
|
+
system: args.prepared.system,
|
|
347
|
+
config: args.config,
|
|
348
|
+
outputPath: args.prepared.artifacts.baselineEvalJson,
|
|
349
|
+
reporter: args.runReporter,
|
|
350
|
+
maxExamples: args.prepared.maxEvalExamples,
|
|
351
|
+
evalSplit: args.prepared.metadata.eval_split,
|
|
352
|
+
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function runTrainStage(args) {
|
|
356
|
+
if (!args.force && await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
165
357
|
await updateRun({
|
|
358
|
+
store: args.store,
|
|
359
|
+
reporter: args.reporter,
|
|
360
|
+
request: args.prepared.request,
|
|
166
361
|
status: "training",
|
|
167
362
|
stage: "training",
|
|
168
|
-
message:
|
|
169
|
-
details: {
|
|
363
|
+
message: "Reusing existing training result.",
|
|
364
|
+
details: { path: args.prepared.artifacts.trainingReportJson },
|
|
170
365
|
});
|
|
171
|
-
|
|
366
|
+
return trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
367
|
+
}
|
|
368
|
+
await updateRun({
|
|
369
|
+
store: args.store,
|
|
370
|
+
reporter: args.reporter,
|
|
371
|
+
request: args.prepared.request,
|
|
372
|
+
status: "training",
|
|
373
|
+
stage: "training",
|
|
374
|
+
message: args.config.dryRun ? "Recording dry-run training result." : "Launching local training process.",
|
|
375
|
+
details: { training_backend: args.config.training.backend, dry_run: args.config.dryRun },
|
|
376
|
+
});
|
|
377
|
+
const training = await launchProcessTraining({
|
|
378
|
+
request: args.prepared.request,
|
|
379
|
+
artifacts: args.prepared.artifacts,
|
|
380
|
+
config: args.config,
|
|
381
|
+
reporter: args.runReporter,
|
|
382
|
+
});
|
|
383
|
+
await writeJson(args.prepared.artifacts.trainingReportJson, training);
|
|
384
|
+
return training;
|
|
385
|
+
}
|
|
386
|
+
async function writeExternalTrainingReport(args) {
|
|
387
|
+
const training = trainingReportSchema.parse({
|
|
388
|
+
provider: args.config.training.backend === "command" ? "local-command" : "local-uv",
|
|
389
|
+
training_job_name: `external-${args.prepared.request.run_id}`,
|
|
390
|
+
model_artifact_uri: args.modelArtifact,
|
|
391
|
+
base_model_artifact_uri: args.config.paths.baseModel ? fileUri(args.config.paths.baseModel) : undefined,
|
|
392
|
+
artifact_metadata: {
|
|
393
|
+
...(args.config.training.artifact ?? {}),
|
|
394
|
+
notes: args.config.training.artifact?.notes ?? "External model artifact supplied with --model-artifact.",
|
|
395
|
+
},
|
|
396
|
+
metrics: { external_model_artifact: true },
|
|
397
|
+
exit_code: null,
|
|
398
|
+
log_uri: fileUri(args.prepared.artifacts.trainingReportJson),
|
|
399
|
+
});
|
|
400
|
+
await writeJson(args.prepared.artifacts.trainingReportJson, training);
|
|
401
|
+
return training;
|
|
402
|
+
}
|
|
403
|
+
async function runCandidateStage(args) {
|
|
404
|
+
if (!args.force && !args.modelArtifact && await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
172
405
|
await updateRun({
|
|
406
|
+
store: args.store,
|
|
407
|
+
reporter: args.reporter,
|
|
408
|
+
request: args.prepared.request,
|
|
173
409
|
status: "evaluating_candidate",
|
|
174
410
|
stage: "evaluating_candidate",
|
|
175
|
-
message: "
|
|
176
|
-
details: {
|
|
411
|
+
message: "Reusing existing candidate evaluation.",
|
|
412
|
+
details: { path: args.prepared.artifacts.candidateEvalJson },
|
|
177
413
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
outputPath: artifacts.candidateEvalJson,
|
|
187
|
-
reporter: runReporter,
|
|
188
|
-
maxExamples: maxEvalExamples,
|
|
189
|
-
evalSplit,
|
|
190
|
-
sampleSeed: evalSampleSeed,
|
|
414
|
+
return evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
|
|
415
|
+
}
|
|
416
|
+
let training;
|
|
417
|
+
if (args.modelArtifact) {
|
|
418
|
+
training = await writeExternalTrainingReport({
|
|
419
|
+
prepared: args.prepared,
|
|
420
|
+
config: args.config,
|
|
421
|
+
modelArtifact: args.modelArtifact,
|
|
191
422
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
423
|
+
}
|
|
424
|
+
else if (await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
425
|
+
training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
throw new Error("candidate stage requires training output or --model-artifact.");
|
|
429
|
+
}
|
|
430
|
+
const modelArtifact = training.model_artifact_uri;
|
|
431
|
+
if (!modelArtifact)
|
|
432
|
+
throw new Error("candidate stage requires a model_artifact_uri in training-report.json or --model-artifact.");
|
|
433
|
+
await updateRun({
|
|
434
|
+
store: args.store,
|
|
435
|
+
reporter: args.reporter,
|
|
436
|
+
request: args.prepared.request,
|
|
437
|
+
status: "evaluating_candidate",
|
|
438
|
+
stage: "evaluating_candidate",
|
|
439
|
+
message: "Running candidate evaluation.",
|
|
440
|
+
details: { model_artifact_uri: modelArtifact },
|
|
441
|
+
});
|
|
442
|
+
return evaluateExamples({
|
|
443
|
+
kind: "candidate",
|
|
444
|
+
modelId: modelArtifact,
|
|
445
|
+
baseModelId: args.prepared.baseModelForEvaluation,
|
|
446
|
+
adapterPath: modelArtifact,
|
|
447
|
+
examples: args.prepared.examples,
|
|
448
|
+
system: args.prepared.system,
|
|
449
|
+
config: args.config,
|
|
450
|
+
outputPath: args.prepared.artifacts.candidateEvalJson,
|
|
451
|
+
reporter: args.runReporter,
|
|
452
|
+
maxExamples: args.prepared.maxEvalExamples,
|
|
453
|
+
evalSplit: args.prepared.metadata.eval_split,
|
|
454
|
+
sampleSeed: args.prepared.metadata.eval_sample_seed,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async function runScoreStage(args) {
|
|
458
|
+
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
459
|
+
throw new Error("score stage requires baseline-eval.json. Run --stage baseline first.");
|
|
460
|
+
}
|
|
461
|
+
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
462
|
+
throw new Error("score stage requires candidate-eval.json. Run --stage candidate first.");
|
|
463
|
+
}
|
|
464
|
+
await updateRun({
|
|
465
|
+
store: args.store,
|
|
466
|
+
reporter: args.reporter,
|
|
467
|
+
request: args.prepared.request,
|
|
468
|
+
status: "scoring",
|
|
469
|
+
stage: "scoring",
|
|
470
|
+
message: "Rescoring existing baseline and candidate outputs.",
|
|
471
|
+
details: { scoring_mode: args.config.evaluation.scoring.mode },
|
|
472
|
+
});
|
|
473
|
+
const baseline = await rescoreEvalReport({
|
|
474
|
+
report: evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson)),
|
|
475
|
+
config: args.config,
|
|
476
|
+
outputPath: args.prepared.artifacts.baselineEvalJson,
|
|
477
|
+
system: args.prepared.system,
|
|
478
|
+
reporter: args.runReporter,
|
|
479
|
+
});
|
|
480
|
+
const candidate = await rescoreEvalReport({
|
|
481
|
+
report: evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson)),
|
|
482
|
+
config: args.config,
|
|
483
|
+
outputPath: args.prepared.artifacts.candidateEvalJson,
|
|
484
|
+
system: args.prepared.system,
|
|
485
|
+
reporter: args.runReporter,
|
|
486
|
+
});
|
|
487
|
+
return { baseline, candidate };
|
|
488
|
+
}
|
|
489
|
+
async function runReportStage(args) {
|
|
490
|
+
if (!await pathExists(args.prepared.artifacts.baselineEvalJson)) {
|
|
491
|
+
throw new Error("report stage requires baseline-eval.json. Run --stage baseline first.");
|
|
492
|
+
}
|
|
493
|
+
if (!await pathExists(args.prepared.artifacts.candidateEvalJson)) {
|
|
494
|
+
throw new Error("report stage requires candidate-eval.json. Run --stage candidate first.");
|
|
495
|
+
}
|
|
496
|
+
if (!await pathExists(args.prepared.artifacts.trainingReportJson)) {
|
|
497
|
+
throw new Error("report stage requires training-report.json. Run --stage train first, or --stage candidate --model-artifact <path>.");
|
|
498
|
+
}
|
|
499
|
+
if (args.emitReportingEvent) {
|
|
500
|
+
await updateRun({
|
|
501
|
+
store: args.store,
|
|
502
|
+
reporter: args.reporter,
|
|
503
|
+
request: args.prepared.request,
|
|
504
|
+
status: "reporting",
|
|
505
|
+
stage: "reporting",
|
|
506
|
+
message: "Writing run report.",
|
|
507
|
+
details: { report_path: args.prepared.artifacts.runReportJson },
|
|
230
508
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
509
|
+
}
|
|
510
|
+
const baseline = evalReportSchema.parse(await readJson(args.prepared.artifacts.baselineEvalJson));
|
|
511
|
+
const candidate = evalReportSchema.parse(await readJson(args.prepared.artifacts.candidateEvalJson));
|
|
512
|
+
const training = trainingReportSchema.parse(await readJson(args.prepared.artifacts.trainingReportJson));
|
|
513
|
+
const comparison = compareEvalReports(baseline, candidate);
|
|
514
|
+
const completedAt = new Date().toISOString();
|
|
515
|
+
const duration = elapsed(args.startedPerf);
|
|
516
|
+
const report = runReportSchema.parse({
|
|
517
|
+
run_id: args.prepared.request.run_id,
|
|
518
|
+
behavior_spec_id: args.prepared.request.behavior_spec_id,
|
|
519
|
+
user_id: args.prepared.request.user_id,
|
|
520
|
+
run_number: args.prepared.request.run_number,
|
|
521
|
+
base_model: args.prepared.request.spec_snapshot.base_model,
|
|
522
|
+
fine_tuned_model_id: training.model_artifact_uri ?? training.training_job_name,
|
|
523
|
+
status: "completed",
|
|
524
|
+
baseline,
|
|
525
|
+
candidate,
|
|
526
|
+
comparison,
|
|
527
|
+
training,
|
|
528
|
+
artifact_uris: {
|
|
529
|
+
dataset: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
530
|
+
baseline_eval: fileUri(args.prepared.artifacts.baselineEvalJson),
|
|
531
|
+
candidate_eval: fileUri(args.prepared.artifacts.candidateEvalJson),
|
|
532
|
+
report: fileUri(args.prepared.artifacts.runReportJson),
|
|
533
|
+
},
|
|
534
|
+
run_metadata: {
|
|
535
|
+
base_model: args.prepared.request.spec_snapshot.base_model,
|
|
536
|
+
fine_tuned_model_id: training.model_artifact_uri ?? training.training_job_name,
|
|
537
|
+
training_method: args.prepared.request.training_method,
|
|
538
|
+
dataset_prebuilt: args.prepared.metadata.dataset_prebuilt,
|
|
539
|
+
dataset_format: args.prepared.metadata.dataset_format,
|
|
540
|
+
dataset_uri: fileUri(args.prepared.artifacts.trainingJsonl),
|
|
541
|
+
spec_example_count: args.prepared.request.spec_snapshot.examples.length,
|
|
542
|
+
training_example_count: args.prepared.metadata.training_example_count,
|
|
543
|
+
eval_examples_total: baseline.eval_examples_total,
|
|
544
|
+
eval_examples_used: baseline.eval_examples_used,
|
|
545
|
+
eval_split: baseline.eval_split,
|
|
546
|
+
eval_sample_seed: baseline.eval_sample_seed ?? null,
|
|
547
|
+
started_at: args.startedAt,
|
|
548
|
+
completed_at: completedAt,
|
|
549
|
+
elapsed_ms: duration.ms,
|
|
550
|
+
elapsed_seconds: duration.seconds,
|
|
551
|
+
},
|
|
552
|
+
created_at: completedAt,
|
|
553
|
+
});
|
|
554
|
+
await writeJson(args.prepared.artifacts.runReportJson, report);
|
|
555
|
+
await args.store.completeRun(report, args.prepared.artifacts.runDir, args.prepared.artifacts.runReportJson);
|
|
556
|
+
await args.reporter?.onEvent?.({
|
|
557
|
+
stage: "completed",
|
|
558
|
+
status: "completed",
|
|
559
|
+
message: "Run completed successfully.",
|
|
560
|
+
details: {
|
|
561
|
+
report_path: args.prepared.artifacts.runReportJson,
|
|
562
|
+
model_id: `local-${args.prepared.request.run_id}`,
|
|
563
|
+
avg_score_delta: comparison.avg_score_delta,
|
|
564
|
+
elapsed_seconds: duration.seconds,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
return report;
|
|
568
|
+
}
|
|
569
|
+
export async function runLocalFineTuneStage(input) {
|
|
570
|
+
const startedPerf = performance.now();
|
|
571
|
+
const startedAt = new Date().toISOString();
|
|
572
|
+
const stage = input.stage ?? "all";
|
|
573
|
+
const prefix = artifactPrefix(input.request);
|
|
574
|
+
const artifacts = resolveRunArtifacts({ artifactRoot: input.config.artifactRoot, prefix });
|
|
575
|
+
const store = createLocalStore(input.config.storeRoot);
|
|
576
|
+
await ensureRunRecord({ request: input.request, artifacts, store, reporter: input.reporter });
|
|
577
|
+
const runReporter = createStoreReporter({ request: input.request, store, reporter: input.reporter });
|
|
578
|
+
try {
|
|
579
|
+
const prepared = await ensurePrepared({
|
|
580
|
+
request: input.request,
|
|
581
|
+
config: input.config,
|
|
582
|
+
artifacts,
|
|
583
|
+
store,
|
|
584
|
+
reporter: input.reporter,
|
|
585
|
+
forcePrepare: Boolean(input.force && (stage === "prepare" || stage === "all")),
|
|
243
586
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
587
|
+
let report;
|
|
588
|
+
if (stage === "prepare") {
|
|
589
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
590
|
+
}
|
|
591
|
+
if (stage === "baseline" || stage === "all") {
|
|
592
|
+
await runBaselineStage({
|
|
593
|
+
prepared,
|
|
594
|
+
config: input.config,
|
|
595
|
+
store,
|
|
596
|
+
reporter: input.reporter,
|
|
597
|
+
runReporter,
|
|
598
|
+
force: Boolean(input.force),
|
|
599
|
+
});
|
|
600
|
+
if (stage === "baseline")
|
|
601
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
602
|
+
}
|
|
603
|
+
if (stage === "train" || stage === "all") {
|
|
604
|
+
await runTrainStage({
|
|
605
|
+
prepared,
|
|
606
|
+
config: input.config,
|
|
607
|
+
store,
|
|
608
|
+
reporter: input.reporter,
|
|
609
|
+
runReporter,
|
|
610
|
+
force: Boolean(input.force),
|
|
611
|
+
});
|
|
612
|
+
if (stage === "train")
|
|
613
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
614
|
+
}
|
|
615
|
+
if (stage === "candidate" || stage === "all") {
|
|
616
|
+
await runCandidateStage({
|
|
617
|
+
prepared,
|
|
618
|
+
config: input.config,
|
|
619
|
+
store,
|
|
620
|
+
reporter: input.reporter,
|
|
621
|
+
runReporter,
|
|
622
|
+
force: Boolean(input.force),
|
|
623
|
+
modelArtifact: input.modelArtifact,
|
|
624
|
+
});
|
|
625
|
+
if (stage === "candidate")
|
|
626
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
627
|
+
}
|
|
628
|
+
if (stage === "score") {
|
|
629
|
+
await runScoreStage({
|
|
630
|
+
prepared,
|
|
631
|
+
config: input.config,
|
|
632
|
+
store,
|
|
633
|
+
reporter: input.reporter,
|
|
634
|
+
runReporter,
|
|
635
|
+
});
|
|
636
|
+
return stageResult({ request: input.request, stage, artifacts });
|
|
637
|
+
}
|
|
638
|
+
if (stage === "report" || stage === "all") {
|
|
639
|
+
report = await runReportStage({
|
|
640
|
+
prepared,
|
|
641
|
+
store,
|
|
642
|
+
reporter: input.reporter,
|
|
643
|
+
startedAt,
|
|
644
|
+
startedPerf,
|
|
645
|
+
emitReportingEvent: stage === "report",
|
|
646
|
+
});
|
|
647
|
+
return stageResult({ request: input.request, stage, artifacts, report });
|
|
648
|
+
}
|
|
649
|
+
throw new Error(`Unknown run stage: ${stage}`);
|
|
250
650
|
}
|
|
251
651
|
catch (error) {
|
|
252
|
-
await store.failRun(request.run_id, error instanceof Error ? error.message : String(error)).catch(() => undefined);
|
|
253
|
-
await reporter?.onEvent?.({
|
|
652
|
+
await store.failRun(input.request.run_id, error instanceof Error ? error.message : String(error)).catch(() => undefined);
|
|
653
|
+
await input.reporter?.onEvent?.({
|
|
254
654
|
stage: "failed",
|
|
255
655
|
status: "failed",
|
|
256
656
|
message: error instanceof Error ? error.message : String(error),
|
|
@@ -258,4 +658,38 @@ export async function runLocalFineTune(input) {
|
|
|
258
658
|
throw error;
|
|
259
659
|
}
|
|
260
660
|
}
|
|
661
|
+
function stageResult(args) {
|
|
662
|
+
return {
|
|
663
|
+
request: args.request,
|
|
664
|
+
stage: args.stage,
|
|
665
|
+
report: args.report,
|
|
666
|
+
reportPath: args.report ? args.artifacts.runReportJson : undefined,
|
|
667
|
+
artifactDir: dirname(args.artifacts.runReportJson),
|
|
668
|
+
artifacts: {
|
|
669
|
+
training_jsonl: args.artifacts.trainingJsonl,
|
|
670
|
+
stage_metadata: args.artifacts.stageMetadataJson,
|
|
671
|
+
training_report: args.artifacts.trainingReportJson,
|
|
672
|
+
baseline_eval: args.artifacts.baselineEvalJson,
|
|
673
|
+
candidate_eval: args.artifacts.candidateEvalJson,
|
|
674
|
+
report: args.artifacts.runReportJson,
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
export async function runLocalFineTune(input) {
|
|
679
|
+
const result = await runLocalFineTuneStage({
|
|
680
|
+
request: input.request,
|
|
681
|
+
config: input.config,
|
|
682
|
+
reporter: input.reporter,
|
|
683
|
+
stage: "all",
|
|
684
|
+
});
|
|
685
|
+
if (!result.report || !result.reportPath) {
|
|
686
|
+
throw new Error("Full local fine-tune run did not produce a report.");
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
request: result.request,
|
|
690
|
+
report: result.report,
|
|
691
|
+
reportPath: result.reportPath,
|
|
692
|
+
artifactDir: result.artifactDir,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
261
695
|
//# sourceMappingURL=orchestrator.js.map
|