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