textopt 0.0.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/LICENSE +21 -0
- package/README.md +509 -0
- package/dist/bootstrap-search/index.cjs +308 -0
- package/dist/bootstrap-search/index.d.cts +162 -0
- package/dist/bootstrap-search/index.d.mts +162 -0
- package/dist/bootstrap-search/index.mjs +307 -0
- package/dist/cache-CuSo0NJ8.d.cts +24 -0
- package/dist/cache-CuSo0NJ8.d.mts +24 -0
- package/dist/concurrency-C-cFzWW2.cjs +44 -0
- package/dist/concurrency-D58PWeSk.mjs +39 -0
- package/dist/demos-B0pVQjYC.d.mts +88 -0
- package/dist/demos-B9BJiNKz.cjs +143 -0
- package/dist/demos-BTuzFNsp.d.cts +88 -0
- package/dist/demos-Degx6UmP.mjs +126 -0
- package/dist/evaluation-BV0nSZVx.mjs +521 -0
- package/dist/evaluation-OZOp6TB7.cjs +598 -0
- package/dist/file-cache.cjs +70 -0
- package/dist/file-cache.d.cts +21 -0
- package/dist/file-cache.d.mts +21 -0
- package/dist/file-cache.mjs +69 -0
- package/dist/gepa/index.cjs +1671 -0
- package/dist/gepa/index.d.cts +385 -0
- package/dist/gepa/index.d.mts +385 -0
- package/dist/gepa/index.mjs +1652 -0
- package/dist/index.cjs +266 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.mts +221 -0
- package/dist/index.mjs +245 -0
- package/dist/math-COOofUyv.cjs +101 -0
- package/dist/math-DhrDmpFS.mjs +78 -0
- package/dist/mipro/index.cjs +739 -0
- package/dist/mipro/index.d.cts +372 -0
- package/dist/mipro/index.d.mts +372 -0
- package/dist/mipro/index.mjs +736 -0
- package/dist/opro/index.cjs +487 -0
- package/dist/opro/index.d.cts +230 -0
- package/dist/opro/index.d.mts +230 -0
- package/dist/opro/index.mjs +485 -0
- package/dist/optimizer-B7SpRwl7.d.cts +288 -0
- package/dist/optimizer-DqCoth_w.d.mts +288 -0
- package/dist/random-search/index.cjs +321 -0
- package/dist/random-search/index.d.cts +156 -0
- package/dist/random-search/index.d.mts +156 -0
- package/dist/random-search/index.mjs +319 -0
- package/dist/reflection-CQToe-5B.d.cts +283 -0
- package/dist/reflection-Cr_upzU0.d.mts +283 -0
- package/dist/reflection-DRfbk6hu.cjs +249 -0
- package/dist/reflection-mwMhrjs_.mjs +214 -0
- package/dist/rng-BR5MOedA.d.cts +22 -0
- package/dist/rng-BR5MOedA.d.mts +22 -0
- package/dist/rng-DbA_rPIo.cjs +67 -0
- package/dist/rng-Dtc5eZ_W.mjs +62 -0
- package/dist/sampling-CfHt7Gue.mjs +59 -0
- package/dist/sampling-DFo_7RNJ.d.mts +23 -0
- package/dist/sampling-Dars7ctR.cjs +64 -0
- package/dist/sampling-axOwfZf5.d.cts +23 -0
- package/dist/simba/index.cjs +709 -0
- package/dist/simba/index.d.cts +289 -0
- package/dist/simba/index.d.mts +289 -0
- package/dist/simba/index.mjs +700 -0
- package/dist/testing.cjs +155 -0
- package/dist/testing.d.cts +53 -0
- package/dist/testing.d.mts +53 -0
- package/dist/testing.mjs +148 -0
- package/dist/text--v4Ffbus.mjs +21 -0
- package/dist/text-CK_HB3su.cjs +26 -0
- package/dist/types-CWv4IQFF.d.cts +129 -0
- package/dist/types-CWv4IQFF.d.mts +129 -0
- package/package.json +135 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { a as requireMeasuredMean, d as createMemoryCache, f as stableHash, i as measuredMean, l as runFingerprint, n as costExhausted, o as createDeadline, p as createBudget, r as createEvaluator, s as assertResumable, t as BudgetExhausted, u as componentNames } from "../evaluation-BV0nSZVx.mjs";
|
|
2
|
+
import { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
|
|
3
|
+
import { n as mean } from "../math-DhrDmpFS.mjs";
|
|
4
|
+
import { n as formatDemos, t as bootstrapDemos } from "../demos-Degx6UmP.mjs";
|
|
5
|
+
import { t as parseProposedText } from "../text--v4Ffbus.mjs";
|
|
6
|
+
import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
|
|
7
|
+
import { t as createEpochShuffledSampler } from "../sampling-CfHt7Gue.mjs";
|
|
8
|
+
//#region src/mipro/tpe.ts
|
|
9
|
+
const DEFAULT_SAMPLES = 24;
|
|
10
|
+
const GOOD_FRACTION = .1;
|
|
11
|
+
const MAX_GOOD = 25;
|
|
12
|
+
const DEFAULT_STARTUP = 10;
|
|
13
|
+
const DEFAULT_PRIOR_WEIGHT = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Tree-structured Parzen Estimator over a categorical space.
|
|
16
|
+
*
|
|
17
|
+
* The search space here is a menu index per component. A TPE splits the
|
|
18
|
+
* observations into the good ones and the rest, models the density of each,
|
|
19
|
+
* and proposes the configuration that maximizes their ratio: sample where
|
|
20
|
+
* good configurations live, prefer where bad ones do not.
|
|
21
|
+
*
|
|
22
|
+
* The densities are mixtures with one kernel centred on each observation,
|
|
23
|
+
* rather than one histogram per component. That distinction is the whole
|
|
24
|
+
* reason this optimizer exists. Per-component histograms only record how often
|
|
25
|
+
* an option appears among good trials, so a space where every option is
|
|
26
|
+
* equally common and only the *pairing* matters looks completely flat to them.
|
|
27
|
+
* A kernel centred on an observed configuration keeps its components together,
|
|
28
|
+
* so "B works, but only alongside A" survives into the proposal. It is what
|
|
29
|
+
* Optuna calls a multivariate sampler, and what MIPROv2 turns on.
|
|
30
|
+
*
|
|
31
|
+
* What this buys over sampling every combination: a candidate with five
|
|
32
|
+
* components and four options each has 1024 configurations and a budget for
|
|
33
|
+
* perhaps thirty evaluations. Per-component hill climbing cannot see
|
|
34
|
+
* interactions at all; enumerating cannot afford to. This can do both, at the
|
|
35
|
+
* cost of being an estimate.
|
|
36
|
+
*/
|
|
37
|
+
function proposeConfiguration(args) {
|
|
38
|
+
const { observations, menuSizes, gamma, samples = DEFAULT_SAMPLES, startupTrials = DEFAULT_STARTUP, priorWeight = DEFAULT_PRIOR_WEIGHT, multivariate = true, rng } = args;
|
|
39
|
+
for (const size of menuSizes) if (!Number.isInteger(size) || size < 1) throw new Error(`every component needs a menu of at least one option, received ${size}`);
|
|
40
|
+
if (observations.length < startupTrials) return menuSizes.map((size) => rng.nextInt(size));
|
|
41
|
+
const scores = observations.map((observation) => observation.score);
|
|
42
|
+
if (Math.max(...scores) === Math.min(...scores)) return menuSizes.map((size) => rng.nextInt(size));
|
|
43
|
+
const ranked = [...observations].sort((a, b) => b.score - a.score);
|
|
44
|
+
const requested = gamma === void 0 ? Math.min(Math.ceil(GOOD_FRACTION * ranked.length), MAX_GOOD) : Math.floor(gamma * ranked.length);
|
|
45
|
+
const goodCount = Math.min(ranked.length - 1, Math.max(1, requested));
|
|
46
|
+
const good = ranked.slice(0, goodCount);
|
|
47
|
+
const bad = ranked.slice(goodCount);
|
|
48
|
+
let best;
|
|
49
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
50
|
+
const goodModel = fit({
|
|
51
|
+
observations: good,
|
|
52
|
+
menuSizes,
|
|
53
|
+
priorWeight,
|
|
54
|
+
multivariate
|
|
55
|
+
});
|
|
56
|
+
const badModel = fit({
|
|
57
|
+
observations: bad,
|
|
58
|
+
menuSizes,
|
|
59
|
+
priorWeight,
|
|
60
|
+
multivariate
|
|
61
|
+
});
|
|
62
|
+
for (let sample = 0; sample < samples; sample += 1) {
|
|
63
|
+
const choices = goodModel.sample(rng);
|
|
64
|
+
const score = Math.log(goodModel.density(choices)) - Math.log(badModel.density(choices));
|
|
65
|
+
if (score > bestScore) {
|
|
66
|
+
bestScore = score;
|
|
67
|
+
best = choices;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds the density over a set of observations.
|
|
74
|
+
*
|
|
75
|
+
* Joint: a mixture with one kernel centred on each observation, plus a uniform
|
|
76
|
+
* kernel. Because a kernel keeps an observation's components together, the
|
|
77
|
+
* mixture assigns high density to combinations that were seen together — which
|
|
78
|
+
* is what carries a dependency between components.
|
|
79
|
+
*
|
|
80
|
+
* Independent: one smoothed histogram per component, multiplied. Evidence
|
|
81
|
+
* about a component generalizes across every combination it appears in, which
|
|
82
|
+
* converges far faster when the components really are independent, and cannot
|
|
83
|
+
* express a dependency at all.
|
|
84
|
+
*
|
|
85
|
+
* Both keep a uniform component — Optuna's `consider_prior` — so an option
|
|
86
|
+
* nobody has drawn never falls to zero probability and stays reachable.
|
|
87
|
+
*/
|
|
88
|
+
function fit(args) {
|
|
89
|
+
const { observations, menuSizes, priorWeight, multivariate } = args;
|
|
90
|
+
const kernelCount = observations.length + 1;
|
|
91
|
+
if (multivariate) return {
|
|
92
|
+
density: (choices) => {
|
|
93
|
+
let total = uniformKernel(menuSizes);
|
|
94
|
+
for (const observation of observations) total += kernel({
|
|
95
|
+
choices,
|
|
96
|
+
center: observation.choices,
|
|
97
|
+
menuSizes,
|
|
98
|
+
priorWeight,
|
|
99
|
+
kernelCount
|
|
100
|
+
});
|
|
101
|
+
return total / kernelCount;
|
|
102
|
+
},
|
|
103
|
+
sample: (rng) => {
|
|
104
|
+
const picked = rng.nextInt(kernelCount);
|
|
105
|
+
if (picked === observations.length) return menuSizes.map((size) => rng.nextInt(size));
|
|
106
|
+
const center = observations[picked].choices;
|
|
107
|
+
const share = priorWeight / kernelCount;
|
|
108
|
+
return menuSizes.map((size, component) => drawWeighted(Array.from({ length: size }, (_, option) => option === center[component] ? share + 1 : share), rng));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const histograms = menuSizes.map((size, component) => {
|
|
112
|
+
const counts = new Array(size).fill(priorWeight);
|
|
113
|
+
for (const observation of observations) {
|
|
114
|
+
const choice = observation.choices[component];
|
|
115
|
+
if (choice !== void 0 && choice < size) counts[choice] = counts[choice] + 1;
|
|
116
|
+
}
|
|
117
|
+
const total = observations.length + priorWeight * size;
|
|
118
|
+
return counts.map((count) => count / total);
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
density: (choices) => {
|
|
122
|
+
let product = 1;
|
|
123
|
+
for (let component = 0; component < histograms.length; component += 1) {
|
|
124
|
+
const histogram = histograms[component];
|
|
125
|
+
product *= histogram[choices[component]];
|
|
126
|
+
}
|
|
127
|
+
return product;
|
|
128
|
+
},
|
|
129
|
+
sample: (rng) => histograms.map((histogram) => drawWeighted(histogram, rng))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* One kernel of the mixture, centred on a single observation.
|
|
134
|
+
*
|
|
135
|
+
* The prior is spread across the kernels rather than added whole to each, so
|
|
136
|
+
* `share` shrinks as the mixture grows and a kernel sits more sharply on what
|
|
137
|
+
* it observed: matched and unmatched options stand at `1 + kernelCount /
|
|
138
|
+
* priorWeight`, which is 2:1 for a lone kernel and 13:1 once twelve
|
|
139
|
+
* observations back it. Optuna spells the same thing as a weight matrix filled
|
|
140
|
+
* with `prior_weight / n_kernels` and incremented at each observed choice
|
|
141
|
+
* (`_calculate_categorical_distributions`); dividing the prior by the count is
|
|
142
|
+
* what makes accumulated evidence narrow the density instead of leaving it as
|
|
143
|
+
* flat as it was on the first trial.
|
|
144
|
+
*/
|
|
145
|
+
function kernel(args) {
|
|
146
|
+
const { choices, center, menuSizes, priorWeight, kernelCount } = args;
|
|
147
|
+
const share = priorWeight / kernelCount;
|
|
148
|
+
let product = 1;
|
|
149
|
+
for (let component = 0; component < menuSizes.length; component += 1) {
|
|
150
|
+
const size = menuSizes[component];
|
|
151
|
+
const matched = choices[component] === center[component];
|
|
152
|
+
product *= (share + (matched ? 1 : 0)) / (share * size + 1);
|
|
153
|
+
}
|
|
154
|
+
return product;
|
|
155
|
+
}
|
|
156
|
+
function uniformKernel(menuSizes) {
|
|
157
|
+
let product = 1;
|
|
158
|
+
for (const size of menuSizes) product /= size;
|
|
159
|
+
return product;
|
|
160
|
+
}
|
|
161
|
+
function drawWeighted(weights, rng) {
|
|
162
|
+
return rng.weighted(weights.map((_, option) => option), weights);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/mipro/optimize.ts
|
|
166
|
+
const DEFAULT_INSTRUCTIONS = 3;
|
|
167
|
+
const DEFAULT_MINIBATCH_SIZE = 35;
|
|
168
|
+
const DEFAULT_MAX_TRIALS = 30;
|
|
169
|
+
const DEFAULT_FULL_EVAL_INTERVAL = 5;
|
|
170
|
+
const DEFAULT_DEMO_SETS = 3;
|
|
171
|
+
const DEFAULT_MAX_DEMOS = 4;
|
|
172
|
+
const DEFAULT_EXEMPLARS = 3;
|
|
173
|
+
const DEFAULT_SUMMARY_EXAMPLES = 10;
|
|
174
|
+
/**
|
|
175
|
+
* Style hints, one per generated instruction. Drawing four instructions from
|
|
176
|
+
* one prompt yields four rewordings of one idea; varying the hint is what
|
|
177
|
+
* makes the menu a spread of approaches instead.
|
|
178
|
+
*/
|
|
179
|
+
const DEFAULT_TIPS = [
|
|
180
|
+
"Be concise. Say only what changes the output.",
|
|
181
|
+
"Be specific and detailed. Spell out the edge cases and the output format.",
|
|
182
|
+
"Describe the reasoning the component should do before it answers.",
|
|
183
|
+
"State the constraints as hard rules the component must never break.",
|
|
184
|
+
"Write it as a role description: who the component is and what it cares about."
|
|
185
|
+
];
|
|
186
|
+
/**
|
|
187
|
+
* Joint search over a fixed menu, guided by a surrogate.
|
|
188
|
+
*
|
|
189
|
+
* The gap this fills is interaction between components. Reflective search
|
|
190
|
+
* updates one component per iteration and screens it in isolation, so a pair
|
|
191
|
+
* of components that only pay off together is invisible to it — a routing rule
|
|
192
|
+
* and the prompt it routes to, an output format and the instruction that
|
|
193
|
+
* assumes it. Merge recombines lineages after the fact but never proposes a
|
|
194
|
+
* joint move.
|
|
195
|
+
*
|
|
196
|
+
* This search proposes a menu of options per component up front, then treats
|
|
197
|
+
* the choice of one option per component as a single categorical
|
|
198
|
+
* configuration and lets a TPE decide which to spend a trial on. Trials run on
|
|
199
|
+
* minibatches; a configuration that beats the best minibatch reading earns a
|
|
200
|
+
* full sweep before it can become the incumbent, so the number reported is
|
|
201
|
+
* never a lucky minibatch.
|
|
202
|
+
*
|
|
203
|
+
* What it gives up is the ability to write text it did not think of at the
|
|
204
|
+
* start. The menu is fixed at trial one — reflective search keeps writing new
|
|
205
|
+
* text for the whole run. Neither dominates; they fail differently.
|
|
206
|
+
*/
|
|
207
|
+
var MiproOptimizer = class {
|
|
208
|
+
#config;
|
|
209
|
+
constructor(config = {}) {
|
|
210
|
+
assertConfig(config);
|
|
211
|
+
this.#config = config;
|
|
212
|
+
}
|
|
213
|
+
async optimize(task) {
|
|
214
|
+
return runMipro({
|
|
215
|
+
config: this.#config,
|
|
216
|
+
task
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function buildMiproPrompt(args) {
|
|
221
|
+
const { componentName, seedText, exemplars, tip, siblings = {}, datasetSummary } = args;
|
|
222
|
+
const others = Object.entries(siblings).filter(([, text]) => text.length > 0);
|
|
223
|
+
return [
|
|
224
|
+
`I am writing the "${componentName}" component of a larger system. Here is the instruction it currently uses:`,
|
|
225
|
+
"",
|
|
226
|
+
"<current_instruction>",
|
|
227
|
+
seedText,
|
|
228
|
+
"</current_instruction>",
|
|
229
|
+
...others.length === 0 ? [] : [
|
|
230
|
+
"",
|
|
231
|
+
"The rest of the system reads as follows. Write something that fits alongside it rather than repeating or contradicting it:",
|
|
232
|
+
"",
|
|
233
|
+
"<system>",
|
|
234
|
+
others.map(([name, text]) => `<${name}>\n${text}\n</${name}>`).join("\n"),
|
|
235
|
+
"</system>"
|
|
236
|
+
],
|
|
237
|
+
...datasetSummary === void 0 ? [] : [
|
|
238
|
+
"",
|
|
239
|
+
"Here is what the data it runs on looks like:",
|
|
240
|
+
"",
|
|
241
|
+
"<dataset_summary>",
|
|
242
|
+
datasetSummary,
|
|
243
|
+
"</dataset_summary>"
|
|
244
|
+
],
|
|
245
|
+
...exemplars.length === 0 ? [] : [
|
|
246
|
+
"",
|
|
247
|
+
"Here are examples of the inputs this component receives:",
|
|
248
|
+
"",
|
|
249
|
+
"<inputs>",
|
|
250
|
+
exemplars.join("\n\n"),
|
|
251
|
+
"</inputs>"
|
|
252
|
+
],
|
|
253
|
+
"",
|
|
254
|
+
"Write an alternative instruction for this component — a different approach to the same job, not an edit of the one above.",
|
|
255
|
+
`Follow this style: ${tip}`,
|
|
256
|
+
"",
|
|
257
|
+
"Return only the new instruction, inside a ``` block."
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
function buildDatasetSummaryPrompt(examples) {
|
|
261
|
+
return [
|
|
262
|
+
"Below are entries from a dataset a system is being tuned against.",
|
|
263
|
+
"",
|
|
264
|
+
"<examples>",
|
|
265
|
+
examples.join("\n\n"),
|
|
266
|
+
"</examples>",
|
|
267
|
+
"",
|
|
268
|
+
"Describe what this dataset is: what the inputs are, what varies between them, and what answering one well requires. Two or three sentences, concrete rather than generic.",
|
|
269
|
+
"",
|
|
270
|
+
"Return only the description, inside a ``` block."
|
|
271
|
+
].join("\n");
|
|
272
|
+
}
|
|
273
|
+
async function runMipro(args) {
|
|
274
|
+
const { config, task } = args;
|
|
275
|
+
const { instructionsPerComponent = DEFAULT_INSTRUCTIONS, minibatchSize = DEFAULT_MINIBATCH_SIZE, maxTrials = DEFAULT_MAX_TRIALS, startupTrials, gamma, surrogateSamples, multivariate, fullEvalInterval = DEFAULT_FULL_EVAL_INTERVAL, demoSets = DEFAULT_DEMO_SETS, maxDemos = DEFAULT_MAX_DEMOS, demoMinScore, exemplars = DEFAULT_EXEMPLARS, datasetSummary = true, summaryExamples = DEFAULT_SUMMARY_EXAMPLES, concurrency = 1, seed = 0, buildPrompt = buildMiproPrompt, tips = DEFAULT_TIPS, trackBestOutputs = false, checkpointCache = true } = config;
|
|
276
|
+
const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, componentOptions, demoComponents, renderDemo, goldOutput, maxMetricCalls, renderDatum = renderDefault, batchSampler = createEpochShuffledSampler({ minibatchSize }), cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
|
|
277
|
+
const deadline = createDeadline({ maxWallClockMs });
|
|
278
|
+
const components = componentNames(seedCandidate);
|
|
279
|
+
if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
|
|
280
|
+
if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
|
|
281
|
+
if (components.length === 0) throw new Error("optimize requires a seed candidate with at least one component");
|
|
282
|
+
if (testSet !== void 0 && testSet.length === 0) throw new Error("optimize requires a non-empty testSet when one is given; omit it to skip held-out evaluation");
|
|
283
|
+
for (const name of demoComponents ?? []) if (componentOptions?.[name] !== void 0) throw new Error(`component "${name}" is listed in both demoComponents and componentOptions; pick one source for its menu`);
|
|
284
|
+
const trainingIds = trainingSet.map((datum, index) => instanceId({
|
|
285
|
+
datum,
|
|
286
|
+
index
|
|
287
|
+
}));
|
|
288
|
+
const validationIds = validationSet.map((datum, index) => instanceId({
|
|
289
|
+
datum,
|
|
290
|
+
index
|
|
291
|
+
}));
|
|
292
|
+
const testIds = testSet?.map((datum, index) => instanceId({
|
|
293
|
+
datum,
|
|
294
|
+
index
|
|
295
|
+
})) ?? [];
|
|
296
|
+
const fingerprint = runFingerprint({
|
|
297
|
+
seedCandidate,
|
|
298
|
+
trainingIds,
|
|
299
|
+
validationIds,
|
|
300
|
+
seed,
|
|
301
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace }
|
|
302
|
+
});
|
|
303
|
+
assertResumable({
|
|
304
|
+
fingerprint,
|
|
305
|
+
...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
|
|
306
|
+
});
|
|
307
|
+
const rng = createSeededRng(seed, resumeFrom?.rngState);
|
|
308
|
+
const budget = createBudget({
|
|
309
|
+
maxMetricCalls,
|
|
310
|
+
spent: resumeFrom?.metricCalls ?? 0
|
|
311
|
+
});
|
|
312
|
+
const evaluationCache = cache === false ? void 0 : cache ?? createMemoryCache();
|
|
313
|
+
const evaluator = createEvaluator({
|
|
314
|
+
adapter,
|
|
315
|
+
budget,
|
|
316
|
+
...retry === void 0 ? {} : { retry },
|
|
317
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace },
|
|
318
|
+
...evaluationCache === void 0 ? {} : { cache: evaluationCache },
|
|
319
|
+
trackOutputs: trackBestOutputs,
|
|
320
|
+
cacheHits: resumeFrom?.cacheHits ?? 0,
|
|
321
|
+
...signal === void 0 ? {} : { signal },
|
|
322
|
+
onEvaluation: (event) => onEvent?.({
|
|
323
|
+
type: "evaluation",
|
|
324
|
+
...event
|
|
325
|
+
})
|
|
326
|
+
});
|
|
327
|
+
evaluator.restore(resumeFrom?.cache ?? []);
|
|
328
|
+
if (resumeFrom?.sampler !== void 0) batchSampler.restore?.(resumeFrom.sampler);
|
|
329
|
+
onEvent?.({
|
|
330
|
+
type: "start",
|
|
331
|
+
components,
|
|
332
|
+
validationSetSize: validationSet.length
|
|
333
|
+
});
|
|
334
|
+
const shown = rng.sample(trainingSet, Math.min(exemplars, trainingSet.length)).map(renderDatum);
|
|
335
|
+
let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
|
|
336
|
+
const menu = {};
|
|
337
|
+
const demoNames = new Set(demoComponents ?? []);
|
|
338
|
+
const proposing = components.some((name) => !demoNames.has(name) && componentOptions?.[name] === void 0);
|
|
339
|
+
let summary;
|
|
340
|
+
if (resumeFrom === void 0 && datasetSummary && proposing && trainingSet.length > 0) {
|
|
341
|
+
reflectionCalls += 1;
|
|
342
|
+
summary = parseProposedText(await reflect({
|
|
343
|
+
prompt: buildDatasetSummaryPrompt(rng.sample(trainingSet, Math.min(summaryExamples, trainingSet.length)).map(renderDatum)),
|
|
344
|
+
...signal === void 0 ? {} : { signal }
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
let bootstrapMetricCalls = resumeFrom?.bootstrapMetricCalls ?? 0;
|
|
348
|
+
/**
|
|
349
|
+
* Builds a demo component's menu from rollouts the metric rewarded.
|
|
350
|
+
*
|
|
351
|
+
* Each set gets its own harvesting pass over a freshly shuffled training set, as
|
|
352
|
+
* MIPROv2 does. Drawing every set from a single pool would be cheaper and
|
|
353
|
+
* identical under deterministic scoring, but a system that answers at
|
|
354
|
+
* temperature does not give the same verdict twice: a second pass can turn a
|
|
355
|
+
* previously failing example into a demo, and one pass can never show it.
|
|
356
|
+
* Sizes vary across the sets because more demos is not monotonically better
|
|
357
|
+
* — a long block crowds out the instruction, and which length wins is
|
|
358
|
+
* exactly what the search settles.
|
|
359
|
+
*
|
|
360
|
+
* Not covered here: MIPROv2 also builds label-only sets from gold outputs,
|
|
361
|
+
* and pads bootstrapped sets with them. A gold output is something only the
|
|
362
|
+
* adapter knows, so there is nothing generic for this to read.
|
|
363
|
+
*/
|
|
364
|
+
async function bootstrapMenu(name) {
|
|
365
|
+
const blocks = [];
|
|
366
|
+
if (goldOutput !== void 0) {
|
|
367
|
+
const labelled = rng.shuffle(trainingSet).map((datum) => ({
|
|
368
|
+
input: datum,
|
|
369
|
+
output: goldOutput(datum)
|
|
370
|
+
})).filter((demo) => demo.output !== void 0).slice(0, maxDemos);
|
|
371
|
+
if (labelled.length > 0) blocks.push(formatDemos(labelled, renderDemo === void 0 ? {} : { render: renderDemo }));
|
|
372
|
+
}
|
|
373
|
+
for (let index = 0; index < demoSets; index += 1) {
|
|
374
|
+
const affordable = Math.min(trainingSet.length, budget.remaining() - validationSet.length);
|
|
375
|
+
if (affordable < 1) break;
|
|
376
|
+
const requested = demoSets === 1 ? maxDemos : Math.round(1 + index * (maxDemos - 1) / (demoSets - 1));
|
|
377
|
+
const harvest = await bootstrapDemos({
|
|
378
|
+
adapter,
|
|
379
|
+
candidate: seedCandidate,
|
|
380
|
+
trainingSet,
|
|
381
|
+
...demoMinScore === void 0 ? {} : { minScore: demoMinScore },
|
|
382
|
+
maxDemos: requested,
|
|
383
|
+
maxMetricCalls: affordable,
|
|
384
|
+
rng,
|
|
385
|
+
...renderDemo === void 0 ? {} : { renderDemo },
|
|
386
|
+
...signal === void 0 ? {} : { signal }
|
|
387
|
+
});
|
|
388
|
+
bootstrapMetricCalls += harvest.metricCalls;
|
|
389
|
+
budget.reserve(harvest.metricCalls);
|
|
390
|
+
if (harvest.demos.length > 0) blocks.push(formatDemos(harvest.demos, renderDemo === void 0 ? {} : { render: renderDemo }));
|
|
391
|
+
}
|
|
392
|
+
return [.../* @__PURE__ */ new Set([
|
|
393
|
+
seedCandidate[name],
|
|
394
|
+
"",
|
|
395
|
+
...blocks
|
|
396
|
+
])].filter((text, index) => index === 0 || text.length === 0 || text.includes("<demo>"));
|
|
397
|
+
}
|
|
398
|
+
for (const name of components) {
|
|
399
|
+
const supplied = componentOptions?.[name];
|
|
400
|
+
const restored = resumeFrom?.menu[name];
|
|
401
|
+
if (restored !== void 0) {
|
|
402
|
+
menu[name] = [...restored];
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (demoNames.has(name)) {
|
|
406
|
+
menu[name] = await bootstrapMenu(name);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (supplied !== void 0) {
|
|
410
|
+
menu[name] = [seedCandidate[name], ...supplied];
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const drawn = await mapWithConcurrency({
|
|
414
|
+
items: Array.from({ length: instructionsPerComponent }, (_, index) => index),
|
|
415
|
+
limit: concurrency,
|
|
416
|
+
signal,
|
|
417
|
+
task: async (index) => {
|
|
418
|
+
reflectionCalls += 1;
|
|
419
|
+
return parseProposedText(await reflect({
|
|
420
|
+
prompt: buildPrompt({
|
|
421
|
+
componentName: name,
|
|
422
|
+
seedText: seedCandidate[name],
|
|
423
|
+
exemplars: shown,
|
|
424
|
+
tip: tips[index % tips.length],
|
|
425
|
+
...summary === void 0 || summary.length === 0 ? {} : { datasetSummary: summary },
|
|
426
|
+
siblings: Object.fromEntries(components.filter((other) => other !== name).map((other) => [other, seedCandidate[other]]))
|
|
427
|
+
}),
|
|
428
|
+
...signal === void 0 ? {} : { signal }
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
menu[name] = [seedCandidate[name], ...drawn.filter((text) => text.length > 0)];
|
|
433
|
+
}
|
|
434
|
+
onEvent?.({
|
|
435
|
+
type: "menu",
|
|
436
|
+
menu,
|
|
437
|
+
reflectionCalls
|
|
438
|
+
});
|
|
439
|
+
const menuSizes = components.map((name) => menu[name].length);
|
|
440
|
+
function candidateFor(choices) {
|
|
441
|
+
const candidate = {};
|
|
442
|
+
components.forEach((name, index) => {
|
|
443
|
+
candidate[name] = menu[name][choices[index]];
|
|
444
|
+
});
|
|
445
|
+
return candidate;
|
|
446
|
+
}
|
|
447
|
+
let trial = resumeFrom?.trial ?? 0;
|
|
448
|
+
let fullEvaluations = resumeFrom?.fullEvaluations ?? 0;
|
|
449
|
+
const observations = [...resumeFrom?.observations ?? []];
|
|
450
|
+
const surrogateInput = [...resumeFrom?.surrogateInput ?? []];
|
|
451
|
+
let stopReason = "maxTrials";
|
|
452
|
+
async function fullSweep(candidate, phase) {
|
|
453
|
+
return evaluator.evaluate({
|
|
454
|
+
candidate,
|
|
455
|
+
batch: validationSet,
|
|
456
|
+
ids: validationIds,
|
|
457
|
+
split: "val",
|
|
458
|
+
phase,
|
|
459
|
+
candidateId: null,
|
|
460
|
+
iteration: trial
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const seedEvaluation = resumeFrom === void 0 ? await fullSweep(seedCandidate, "seed") : void 0;
|
|
464
|
+
const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : requireMeasuredMean({
|
|
465
|
+
batch: seedEvaluation,
|
|
466
|
+
phase: "seed"
|
|
467
|
+
});
|
|
468
|
+
if (seedEvaluation !== void 0) fullEvaluations += 1;
|
|
469
|
+
if (resumeFrom === void 0) surrogateInput.push({
|
|
470
|
+
choices: menuSizes.map(() => 0),
|
|
471
|
+
score: seedScore
|
|
472
|
+
});
|
|
473
|
+
let best = resumeFrom?.best ?? seedCandidate;
|
|
474
|
+
let bestScore = resumeFrom?.bestScore ?? seedScore;
|
|
475
|
+
/** Absent on a resumed run until a sweep wins: outputs are not checkpointed. */
|
|
476
|
+
let bestOutputs = seedEvaluation?.outputs;
|
|
477
|
+
const readings = new Map(resumeFrom?.readings ?? []);
|
|
478
|
+
const swept = new Set(resumeFrom?.swept ?? []);
|
|
479
|
+
function takeSnapshot() {
|
|
480
|
+
const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
|
|
481
|
+
const samplerState = batchSampler.state?.();
|
|
482
|
+
return {
|
|
483
|
+
version: 1,
|
|
484
|
+
fingerprint,
|
|
485
|
+
menu: { ...menu },
|
|
486
|
+
best,
|
|
487
|
+
bestScore,
|
|
488
|
+
seedScore,
|
|
489
|
+
trial,
|
|
490
|
+
fullEvaluations,
|
|
491
|
+
reflectionCalls,
|
|
492
|
+
bootstrapMetricCalls,
|
|
493
|
+
metricCalls: budget.spent(),
|
|
494
|
+
cacheHits: evaluator.cacheHits(),
|
|
495
|
+
rngState: rng.state(),
|
|
496
|
+
observations: [...observations],
|
|
497
|
+
surrogateInput: surrogateInput.map((entry) => ({
|
|
498
|
+
choices: [...entry.choices],
|
|
499
|
+
score: entry.score
|
|
500
|
+
})),
|
|
501
|
+
readings: [...readings].map(([key, values]) => [key, [...values]]),
|
|
502
|
+
swept: [...swept],
|
|
503
|
+
...samplerState === void 0 ? {} : { sampler: samplerState },
|
|
504
|
+
...cached === void 0 ? {} : { cache: cached }
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function checkpoint() {
|
|
508
|
+
if (onCheckpoint === void 0) return;
|
|
509
|
+
await onCheckpoint(takeSnapshot());
|
|
510
|
+
}
|
|
511
|
+
await checkpoint();
|
|
512
|
+
/**
|
|
513
|
+
* Full-evaluates the strongest configuration by mean minibatch reading that
|
|
514
|
+
* has not been swept yet. Only a full sweep can move the incumbent, so a
|
|
515
|
+
* lucky minibatch buys a candidate a closer look and nothing more.
|
|
516
|
+
*/
|
|
517
|
+
async function sweepBestUnswept() {
|
|
518
|
+
let bestKey;
|
|
519
|
+
let bestMean = Number.NEGATIVE_INFINITY;
|
|
520
|
+
for (const [key, values] of readings) {
|
|
521
|
+
if (swept.has(key)) continue;
|
|
522
|
+
const value = mean(values);
|
|
523
|
+
if (value > bestMean) {
|
|
524
|
+
bestMean = value;
|
|
525
|
+
bestKey = key;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (bestKey === void 0) return "none";
|
|
529
|
+
if (!budget.canAfford(validationSet.length)) return "unaffordable";
|
|
530
|
+
const choices = bestKey.split(",").map(Number);
|
|
531
|
+
const candidate = candidateFor(choices);
|
|
532
|
+
swept.add(bestKey);
|
|
533
|
+
let evaluation;
|
|
534
|
+
try {
|
|
535
|
+
evaluation = await fullSweep(candidate, "validation");
|
|
536
|
+
} catch (err) {
|
|
537
|
+
if (err instanceof BudgetExhausted) return "budgetExhausted";
|
|
538
|
+
if (signal?.aborted) return "aborted";
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
fullEvaluations += 1;
|
|
542
|
+
const score = measuredMean(evaluation);
|
|
543
|
+
if (score === void 0) return "swept";
|
|
544
|
+
surrogateInput.push({
|
|
545
|
+
choices,
|
|
546
|
+
score
|
|
547
|
+
});
|
|
548
|
+
for (const observation of observations) if (observation.choices.join(",") === bestKey) {
|
|
549
|
+
observation.promoted = true;
|
|
550
|
+
observation.score = score;
|
|
551
|
+
}
|
|
552
|
+
if (score > bestScore) {
|
|
553
|
+
best = candidate;
|
|
554
|
+
bestScore = score;
|
|
555
|
+
bestOutputs = evaluation.outputs;
|
|
556
|
+
onEvent?.({
|
|
557
|
+
type: "incumbent",
|
|
558
|
+
trial,
|
|
559
|
+
choices,
|
|
560
|
+
score
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return "swept";
|
|
564
|
+
}
|
|
565
|
+
while (trial < maxTrials) {
|
|
566
|
+
if (signal?.aborted) {
|
|
567
|
+
stopReason = "aborted";
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
if (costExhausted({
|
|
571
|
+
usage: evaluator.usage(),
|
|
572
|
+
maxCostUsd
|
|
573
|
+
})) {
|
|
574
|
+
stopReason = "costExhausted";
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
if (deadline.exceeded()) {
|
|
578
|
+
stopReason = "deadlineReached";
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
if (!budget.canAfford(minibatchSize)) {
|
|
582
|
+
stopReason = "budgetExhausted";
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
if (!budget.canAfford(validationSet.length)) {
|
|
586
|
+
stopReason = "budgetExhausted";
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const choices = proposeConfiguration({
|
|
590
|
+
observations: surrogateInput,
|
|
591
|
+
menuSizes,
|
|
592
|
+
...gamma === void 0 ? {} : { gamma },
|
|
593
|
+
...surrogateSamples === void 0 ? {} : { samples: surrogateSamples },
|
|
594
|
+
...startupTrials === void 0 ? {} : { startupTrials },
|
|
595
|
+
...multivariate === void 0 ? {} : { multivariate },
|
|
596
|
+
rng
|
|
597
|
+
});
|
|
598
|
+
const candidate = candidateFor(choices);
|
|
599
|
+
const batchIndices = batchSampler({
|
|
600
|
+
trainingSet,
|
|
601
|
+
iteration: trial,
|
|
602
|
+
rng
|
|
603
|
+
});
|
|
604
|
+
let minibatchScore;
|
|
605
|
+
try {
|
|
606
|
+
const evaluation = await evaluator.evaluate({
|
|
607
|
+
candidate,
|
|
608
|
+
batch: batchIndices.map((index) => trainingSet[index]),
|
|
609
|
+
ids: batchIndices.map((index) => trainingIds[index]),
|
|
610
|
+
split: "train",
|
|
611
|
+
phase: "minibatch",
|
|
612
|
+
candidateId: null,
|
|
613
|
+
iteration: trial
|
|
614
|
+
});
|
|
615
|
+
minibatchScore = measuredMean(evaluation);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
if (err instanceof BudgetExhausted) {
|
|
618
|
+
stopReason = "budgetExhausted";
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
if (signal?.aborted) {
|
|
622
|
+
stopReason = "aborted";
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
if (minibatchScore === void 0) {
|
|
628
|
+
trial += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
surrogateInput.push({
|
|
632
|
+
choices,
|
|
633
|
+
score: minibatchScore
|
|
634
|
+
});
|
|
635
|
+
const key = choices.join(",");
|
|
636
|
+
readings.set(key, [...readings.get(key) ?? [], minibatchScore]);
|
|
637
|
+
const observation = {
|
|
638
|
+
trial,
|
|
639
|
+
choices,
|
|
640
|
+
minibatchScore,
|
|
641
|
+
promoted: false
|
|
642
|
+
};
|
|
643
|
+
observations.push(observation);
|
|
644
|
+
onEvent?.({
|
|
645
|
+
type: "trial",
|
|
646
|
+
trial,
|
|
647
|
+
choices,
|
|
648
|
+
minibatchScore,
|
|
649
|
+
promoted: false
|
|
650
|
+
});
|
|
651
|
+
trial += 1;
|
|
652
|
+
await checkpoint();
|
|
653
|
+
if (trial % fullEvalInterval === 0) {
|
|
654
|
+
const outcome = await sweepBestUnswept();
|
|
655
|
+
if (outcome === "budgetExhausted" || outcome === "aborted") {
|
|
656
|
+
stopReason = outcome;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
if (outcome === "unaffordable") {
|
|
660
|
+
stopReason = "budgetExhausted";
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (stopReason === "maxTrials" && !signal?.aborted) await sweepBestUnswept();
|
|
666
|
+
if (signal?.aborted) stopReason = "aborted";
|
|
667
|
+
const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
|
|
668
|
+
candidate: best,
|
|
669
|
+
batch: testSet,
|
|
670
|
+
ids: testIds,
|
|
671
|
+
split: "test",
|
|
672
|
+
phase: "test",
|
|
673
|
+
candidateId: null,
|
|
674
|
+
iteration: trial,
|
|
675
|
+
charge: false
|
|
676
|
+
}));
|
|
677
|
+
onEvent?.({
|
|
678
|
+
type: "finish",
|
|
679
|
+
reason: stopReason,
|
|
680
|
+
bestScore,
|
|
681
|
+
metricCalls: budget.spent(),
|
|
682
|
+
...testScore === void 0 ? {} : { testScore }
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
snapshot: takeSnapshot(),
|
|
686
|
+
bestCandidate: best,
|
|
687
|
+
bestScore,
|
|
688
|
+
usage: evaluator.usage(),
|
|
689
|
+
seedScore,
|
|
690
|
+
...trackBestOutputs ? { bestOutputs } : {},
|
|
691
|
+
...testScore === void 0 ? {} : {
|
|
692
|
+
testScore,
|
|
693
|
+
testMetricCalls: evaluator.unchargedCalls()
|
|
694
|
+
},
|
|
695
|
+
trials: trial,
|
|
696
|
+
menu,
|
|
697
|
+
observations,
|
|
698
|
+
fullEvaluations,
|
|
699
|
+
bootstrapMetricCalls,
|
|
700
|
+
metricCalls: budget.spent(),
|
|
701
|
+
reflectionCalls,
|
|
702
|
+
cacheHits: evaluator.cacheHits(),
|
|
703
|
+
stopReason
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function renderDefault(datum) {
|
|
707
|
+
if (typeof datum === "string") return datum;
|
|
708
|
+
try {
|
|
709
|
+
return JSON.stringify(datum, null, 2) ?? String(datum);
|
|
710
|
+
} catch {
|
|
711
|
+
return String(datum);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function assertConfig(config) {
|
|
715
|
+
const positive = [
|
|
716
|
+
["minibatchSize", config.minibatchSize],
|
|
717
|
+
["maxTrials", config.maxTrials],
|
|
718
|
+
["surrogateSamples", config.surrogateSamples],
|
|
719
|
+
["concurrency", config.concurrency]
|
|
720
|
+
];
|
|
721
|
+
for (const [name, value] of positive) if (value !== void 0 && (!Number.isInteger(value) || value < 1)) throw new Error(`${name} must be a positive integer, received ${value}`);
|
|
722
|
+
const nonNegative = [
|
|
723
|
+
["instructionsPerComponent", config.instructionsPerComponent],
|
|
724
|
+
["startupTrials", config.startupTrials],
|
|
725
|
+
["exemplars", config.exemplars]
|
|
726
|
+
];
|
|
727
|
+
for (const [name, value] of nonNegative) if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new Error(`${name} must be a non-negative integer, received ${value}`);
|
|
728
|
+
if (config.gamma !== void 0 && (!Number.isFinite(config.gamma) || config.gamma <= 0 || config.gamma > 1)) throw new Error(`gamma must be within (0, 1], received ${config.gamma}`);
|
|
729
|
+
if (config.tips !== void 0 && config.tips.length === 0) throw new Error("tips must not be empty");
|
|
730
|
+
}
|
|
731
|
+
function defaultInstanceId(args) {
|
|
732
|
+
const hash = stableHash(args.datum);
|
|
733
|
+
return hash === "" ? String(args.index) : hash;
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
export { MiproOptimizer, buildMiproPrompt, proposeConfiguration };
|