orizu 0.3.6 → 0.3.7
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/dist/help.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/help.ts +1 -1
- package/src/index.ts +1 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/__init__.py +4 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +10 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +423 -21
- package/vendor/skills/orizu-cli/SKILL.md +1 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +2 -1
- package/vendor/skills/orizu-cli/references/optimization-reports.md +3 -3
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +4 -2
package/dist/help.js
CHANGED
|
@@ -259,7 +259,7 @@ export const COMMAND_DOCS = [
|
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
path: ['optimizations', 'run-gepa'],
|
|
262
|
-
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--log-dir logs]',
|
|
262
|
+
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--num-threads auto|N] [--log-dir logs]',
|
|
263
263
|
summary: 'Run the bundled GEPA-style optimizer locally and stream events.',
|
|
264
264
|
group: 'Optimizations',
|
|
265
265
|
},
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ function printPreviewUsage() {
|
|
|
41
41
|
printLine(`\nLocal app preview:\n\n orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]`);
|
|
42
42
|
}
|
|
43
43
|
function printOptimizationUsage() {
|
|
44
|
-
printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`);
|
|
44
|
+
printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--num-threads auto|N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`);
|
|
45
45
|
}
|
|
46
46
|
let cliArgs = process.argv.slice(2);
|
|
47
47
|
let cliJsonOutput = false;
|
package/package.json
CHANGED
package/src/help.ts
CHANGED
|
@@ -284,7 +284,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
284
284
|
},
|
|
285
285
|
{
|
|
286
286
|
path: ['optimizations', 'run-gepa'],
|
|
287
|
-
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--log-dir logs]',
|
|
287
|
+
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--num-threads auto|N] [--log-dir logs]',
|
|
288
288
|
summary: 'Run the bundled GEPA-style optimizer locally and stream events.',
|
|
289
289
|
group: 'Optimizations',
|
|
290
290
|
},
|
package/src/index.ts
CHANGED
|
@@ -288,7 +288,7 @@ function printPreviewUsage() {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
function printOptimizationUsage() {
|
|
291
|
-
printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
|
|
291
|
+
printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--num-threads auto|N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
let cliArgs = process.argv.slice(2)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
from .optimizer import (
|
|
2
2
|
Budget,
|
|
3
3
|
DatasetRow,
|
|
4
|
+
NumThreadsPlan,
|
|
4
5
|
PromptContext,
|
|
5
6
|
RowEvaluation,
|
|
6
7
|
TextGepaConfig,
|
|
7
8
|
TextGepaResult,
|
|
8
9
|
optimize_text_candidate,
|
|
10
|
+
resolve_num_threads,
|
|
9
11
|
)
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
12
14
|
"Budget",
|
|
13
15
|
"DatasetRow",
|
|
16
|
+
"NumThreadsPlan",
|
|
14
17
|
"PromptContext",
|
|
15
18
|
"RowEvaluation",
|
|
16
19
|
"TextGepaConfig",
|
|
17
20
|
"TextGepaResult",
|
|
18
21
|
"optimize_text_candidate",
|
|
22
|
+
"resolve_num_threads",
|
|
19
23
|
]
|
|
@@ -19,6 +19,12 @@ def positive_int(value: str) -> int:
|
|
|
19
19
|
return parsed
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def num_threads_arg(value: str) -> int | str:
|
|
23
|
+
if value.strip().lower() == "auto":
|
|
24
|
+
return "auto"
|
|
25
|
+
return positive_int(value)
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
def read_text_arg(value: str | None) -> str | None:
|
|
23
29
|
if value is None:
|
|
24
30
|
return None
|
|
@@ -54,9 +60,10 @@ def main() -> None:
|
|
|
54
60
|
parser.add_argument("--split-set-id", required=True)
|
|
55
61
|
parser.add_argument("--train-split", default="train")
|
|
56
62
|
parser.add_argument("--val-split", default="validation")
|
|
57
|
-
parser.add_argument("--budget", default="light", choices=["auto", "light", "medium", "
|
|
63
|
+
parser.add_argument("--budget", default="light", choices=["auto", "light", "medium", "heavy"])
|
|
58
64
|
parser.add_argument("--max-iterations", type=positive_int, default=3)
|
|
59
65
|
parser.add_argument("--minibatch-size", type=positive_int, default=3)
|
|
66
|
+
parser.add_argument("--num-threads", type=num_threads_arg, default=TextGepaConfig.num_threads)
|
|
60
67
|
parser.add_argument(
|
|
61
68
|
"--candidate-selection-strategy",
|
|
62
69
|
default=TextGepaConfig.candidate_selection_strategy,
|
|
@@ -102,6 +109,7 @@ def main() -> None:
|
|
|
102
109
|
budget=args.budget,
|
|
103
110
|
max_iterations=args.max_iterations,
|
|
104
111
|
minibatch_size=args.minibatch_size,
|
|
112
|
+
num_threads=args.num_threads,
|
|
105
113
|
candidate_selection_strategy=args.candidate_selection_strategy,
|
|
106
114
|
epsilon=args.epsilon,
|
|
107
115
|
max_metric_calls=args.max_metric_calls,
|
|
@@ -153,6 +161,7 @@ def main() -> None:
|
|
|
153
161
|
"dataset_size": len(trainset) + len(valset),
|
|
154
162
|
"train_count": len(trainset),
|
|
155
163
|
"validation_count": len(valset),
|
|
164
|
+
"num_threads": args.num_threads,
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
run_id = client.start_run(
|
|
@@ -4,9 +4,13 @@ import dataclasses
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
6
|
import math
|
|
7
|
+
import os
|
|
7
8
|
import random
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
8
11
|
import time
|
|
9
12
|
import uuid
|
|
13
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
10
14
|
from dataclasses import dataclass, field
|
|
11
15
|
from typing import Any, Callable, Protocol
|
|
12
16
|
|
|
@@ -40,6 +44,31 @@ Return only the complete updated parameter text, exactly as it should be used by
|
|
|
40
44
|
Do not include analysis, explanation, labels, XML tags, or a surrounding markdown code fence.
|
|
41
45
|
Your response will be used verbatim as the next candidate."""
|
|
42
46
|
|
|
47
|
+
AUTO_NUM_THREADS_HARD_CAP = 8
|
|
48
|
+
AUTO_NUM_THREADS_WORKER_MEMORY_BYTES = 512 * 1024 * 1024
|
|
49
|
+
AUTO_NUM_THREADS_MEMORY_RESERVE_BYTES = 1024 * 1024 * 1024
|
|
50
|
+
AUTO_NUM_THREADS_FD_HEADROOM = 64
|
|
51
|
+
AUTO_NUM_THREADS_FDS_PER_WORKER = 16
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class NumThreadsPlan:
|
|
56
|
+
requested: int | str
|
|
57
|
+
resolved: int
|
|
58
|
+
row_bound: int
|
|
59
|
+
cpu_bound: int
|
|
60
|
+
memory_bound: int | None
|
|
61
|
+
fd_bound: int | None
|
|
62
|
+
hard_cap: int
|
|
63
|
+
worker_memory_bytes: int
|
|
64
|
+
available_memory_bytes: int | None
|
|
65
|
+
total_memory_bytes: int | None
|
|
66
|
+
memory_reserve_bytes: int | None
|
|
67
|
+
limiting_factor: str
|
|
68
|
+
|
|
69
|
+
def to_payload(self) -> dict[str, Any]:
|
|
70
|
+
return dataclasses.asdict(self)
|
|
71
|
+
|
|
43
72
|
|
|
44
73
|
@dataclass(frozen=True)
|
|
45
74
|
class DatasetRow:
|
|
@@ -113,6 +142,34 @@ class ReflectionResult:
|
|
|
113
142
|
candidate_text: str
|
|
114
143
|
|
|
115
144
|
|
|
145
|
+
def _dspy_auto_metric_budget(
|
|
146
|
+
*,
|
|
147
|
+
num_components: int,
|
|
148
|
+
num_candidates: int,
|
|
149
|
+
valset_size: int,
|
|
150
|
+
minibatch_size: int = 35,
|
|
151
|
+
full_eval_steps: int = 5,
|
|
152
|
+
) -> int:
|
|
153
|
+
if num_candidates <= 0:
|
|
154
|
+
raise ValueError("num_candidates must be > 0.")
|
|
155
|
+
if num_components < 0 or valset_size < 0 or minibatch_size < 0:
|
|
156
|
+
raise ValueError("num_components, valset_size, and minibatch_size must be >= 0.")
|
|
157
|
+
if full_eval_steps < 1:
|
|
158
|
+
raise ValueError("full_eval_steps must be >= 1.")
|
|
159
|
+
|
|
160
|
+
num_trials = int(max(2 * (num_components * 2) * math.log2(num_candidates), 1.5 * num_candidates))
|
|
161
|
+
total = valset_size
|
|
162
|
+
total += num_candidates * 5
|
|
163
|
+
total += num_trials * minibatch_size
|
|
164
|
+
if num_trials == 0:
|
|
165
|
+
return total
|
|
166
|
+
|
|
167
|
+
periodic_fulls = (num_trials + 1) // full_eval_steps + 1
|
|
168
|
+
extra_final = 1 if num_trials < full_eval_steps else 0
|
|
169
|
+
total += (periodic_fulls + extra_final) * valset_size
|
|
170
|
+
return total
|
|
171
|
+
|
|
172
|
+
|
|
116
173
|
@dataclass
|
|
117
174
|
class Budget:
|
|
118
175
|
budget_kind: str
|
|
@@ -122,20 +179,35 @@ class Budget:
|
|
|
122
179
|
used_candidate_proposals: int = 0
|
|
123
180
|
|
|
124
181
|
@classmethod
|
|
125
|
-
def from_config(
|
|
182
|
+
def from_config(
|
|
183
|
+
cls,
|
|
184
|
+
config: "TextGepaConfig",
|
|
185
|
+
*,
|
|
186
|
+
trainset_size: int = 0,
|
|
187
|
+
valset_size: int = 0,
|
|
188
|
+
num_components: int = 1,
|
|
189
|
+
) -> "Budget":
|
|
126
190
|
if config.max_metric_calls is not None:
|
|
127
191
|
return cls("max_metric_calls", config.max_metric_calls)
|
|
128
192
|
if config.max_full_evals is not None:
|
|
129
|
-
return cls("
|
|
193
|
+
return cls("max_metric_calls", config.max_full_evals * (trainset_size + valset_size))
|
|
130
194
|
if config.max_candidate_proposals is not None:
|
|
131
195
|
return cls("max_candidate_proposals", config.max_candidate_proposals)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"light":
|
|
135
|
-
"medium":
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
196
|
+
preset = "medium" if config.budget == "auto" else config.budget
|
|
197
|
+
preset_candidates = {
|
|
198
|
+
"light": 6,
|
|
199
|
+
"medium": 12,
|
|
200
|
+
"heavy": 18,
|
|
201
|
+
"high": 18,
|
|
202
|
+
}.get(preset, 6)
|
|
203
|
+
return cls(
|
|
204
|
+
"max_metric_calls",
|
|
205
|
+
_dspy_auto_metric_budget(
|
|
206
|
+
num_components=num_components,
|
|
207
|
+
num_candidates=preset_candidates,
|
|
208
|
+
valset_size=valset_size,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
139
211
|
|
|
140
212
|
@property
|
|
141
213
|
def used(self) -> int:
|
|
@@ -152,9 +224,6 @@ class Budget:
|
|
|
152
224
|
def allows_iteration(self) -> bool:
|
|
153
225
|
return self.remaining > 0
|
|
154
226
|
|
|
155
|
-
def allows_metric_call(self) -> bool:
|
|
156
|
-
return self.budget_kind != "max_metric_calls" or self.remaining > 0
|
|
157
|
-
|
|
158
227
|
def to_payload(self) -> dict[str, Any]:
|
|
159
228
|
return {
|
|
160
229
|
"budget_kind": self.budget_kind,
|
|
@@ -172,6 +241,7 @@ class TextGepaConfig:
|
|
|
172
241
|
budget: str = "light"
|
|
173
242
|
max_iterations: int = 3
|
|
174
243
|
minibatch_size: int = 3
|
|
244
|
+
num_threads: int | str = "auto"
|
|
175
245
|
candidate_selection_strategy: str = "pareto"
|
|
176
246
|
epsilon: float = 0.1
|
|
177
247
|
max_metric_calls: int | None = None
|
|
@@ -316,6 +386,229 @@ def _metric_key(scorer_context: PromptContext) -> str:
|
|
|
316
386
|
return scorer_context.metric_key or "score"
|
|
317
387
|
|
|
318
388
|
|
|
389
|
+
def _positive_int_env(name: str, default: int) -> int:
|
|
390
|
+
raw = os.environ.get(name)
|
|
391
|
+
if raw is None:
|
|
392
|
+
return default
|
|
393
|
+
try:
|
|
394
|
+
value = int(raw)
|
|
395
|
+
except ValueError:
|
|
396
|
+
return default
|
|
397
|
+
return value if value > 0 else default
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _proc_meminfo_bytes(key: str) -> int | None:
|
|
401
|
+
try:
|
|
402
|
+
with open("/proc/meminfo", encoding="utf-8") as handle:
|
|
403
|
+
for line in handle:
|
|
404
|
+
if not line.startswith(f"{key}:"):
|
|
405
|
+
continue
|
|
406
|
+
parts = line.split()
|
|
407
|
+
if len(parts) >= 2:
|
|
408
|
+
return int(parts[1]) * 1024
|
|
409
|
+
except (OSError, ValueError):
|
|
410
|
+
return None
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _sysconf_memory_bytes(pages_name: str) -> int | None:
|
|
415
|
+
try:
|
|
416
|
+
pages = os.sysconf(pages_name)
|
|
417
|
+
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
418
|
+
except (AttributeError, OSError, ValueError):
|
|
419
|
+
return None
|
|
420
|
+
if not isinstance(pages, int) or not isinstance(page_size, int):
|
|
421
|
+
return None
|
|
422
|
+
if pages <= 0 or page_size <= 0:
|
|
423
|
+
return None
|
|
424
|
+
return pages * page_size
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _darwin_available_memory_bytes() -> int | None:
|
|
428
|
+
if sys.platform != "darwin":
|
|
429
|
+
return None
|
|
430
|
+
try:
|
|
431
|
+
output = subprocess.check_output(["vm_stat"], text=True)
|
|
432
|
+
except (OSError, subprocess.SubprocessError):
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
lines = output.splitlines()
|
|
436
|
+
if not lines:
|
|
437
|
+
return None
|
|
438
|
+
page_size_marker = "page size of "
|
|
439
|
+
if page_size_marker not in lines[0]:
|
|
440
|
+
return None
|
|
441
|
+
try:
|
|
442
|
+
page_size = int(lines[0].split(page_size_marker, 1)[1].split()[0])
|
|
443
|
+
except (IndexError, ValueError):
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
available_pages = 0
|
|
447
|
+
available_keys = {
|
|
448
|
+
"Pages free",
|
|
449
|
+
"Pages inactive",
|
|
450
|
+
"Pages speculative",
|
|
451
|
+
"Pages purgeable",
|
|
452
|
+
}
|
|
453
|
+
for line in lines[1:]:
|
|
454
|
+
key, separator, value = line.partition(":")
|
|
455
|
+
if separator != ":" or key not in available_keys:
|
|
456
|
+
continue
|
|
457
|
+
normalized_value = value.strip().rstrip(".").replace(",", "")
|
|
458
|
+
try:
|
|
459
|
+
available_pages += int(normalized_value)
|
|
460
|
+
except ValueError:
|
|
461
|
+
continue
|
|
462
|
+
return available_pages * page_size if available_pages > 0 else None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _available_memory_bytes() -> int | None:
|
|
466
|
+
return (
|
|
467
|
+
_proc_meminfo_bytes("MemAvailable")
|
|
468
|
+
or _darwin_available_memory_bytes()
|
|
469
|
+
or _sysconf_memory_bytes("SC_AVPHYS_PAGES")
|
|
470
|
+
or _total_memory_bytes()
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _total_memory_bytes() -> int | None:
|
|
475
|
+
return _proc_meminfo_bytes("MemTotal") or _sysconf_memory_bytes("SC_PHYS_PAGES")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _open_file_soft_limit() -> int | None:
|
|
479
|
+
try:
|
|
480
|
+
import resource
|
|
481
|
+
except ImportError:
|
|
482
|
+
return None
|
|
483
|
+
try:
|
|
484
|
+
soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
485
|
+
except (OSError, ValueError):
|
|
486
|
+
return None
|
|
487
|
+
if soft_limit <= 0 or soft_limit == resource.RLIM_INFINITY:
|
|
488
|
+
return None
|
|
489
|
+
return int(soft_limit)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _parse_requested_num_threads(requested: int | str) -> int | str:
|
|
493
|
+
if isinstance(requested, int):
|
|
494
|
+
if requested <= 0:
|
|
495
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
496
|
+
return requested
|
|
497
|
+
if isinstance(requested, str):
|
|
498
|
+
normalized = requested.strip().lower()
|
|
499
|
+
if normalized == "auto":
|
|
500
|
+
return "auto"
|
|
501
|
+
try:
|
|
502
|
+
parsed = int(normalized)
|
|
503
|
+
except ValueError as exc:
|
|
504
|
+
raise ValueError("num_threads must be positive or 'auto'") from exc
|
|
505
|
+
if parsed <= 0:
|
|
506
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
507
|
+
return parsed
|
|
508
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def resolve_num_threads(
|
|
512
|
+
requested: int | str,
|
|
513
|
+
*,
|
|
514
|
+
minibatch_size: int,
|
|
515
|
+
validation_count: int,
|
|
516
|
+
cpu_count: int | None = None,
|
|
517
|
+
available_memory_bytes: int | None = None,
|
|
518
|
+
total_memory_bytes: int | None = None,
|
|
519
|
+
fd_limit: int | None = None,
|
|
520
|
+
hard_cap: int | None = None,
|
|
521
|
+
worker_memory_bytes: int | None = None,
|
|
522
|
+
) -> NumThreadsPlan:
|
|
523
|
+
"""Resolve an explicit or automatic row-evaluation thread count for one GEPA run."""
|
|
524
|
+
parsed = _parse_requested_num_threads(requested)
|
|
525
|
+
row_bound = max(1, max(minibatch_size, validation_count))
|
|
526
|
+
detected_cpu_count = cpu_count if cpu_count is not None else os.cpu_count()
|
|
527
|
+
cpu_bound = max(1, detected_cpu_count or 1)
|
|
528
|
+
resolved_hard_cap = hard_cap or _positive_int_env(
|
|
529
|
+
"ORIZU_GEPA_AUTO_THREADS_MAX",
|
|
530
|
+
AUTO_NUM_THREADS_HARD_CAP,
|
|
531
|
+
)
|
|
532
|
+
resolved_worker_memory_bytes = worker_memory_bytes or (
|
|
533
|
+
_positive_int_env(
|
|
534
|
+
"ORIZU_GEPA_WORKER_MEMORY_MB",
|
|
535
|
+
AUTO_NUM_THREADS_WORKER_MEMORY_BYTES // (1024 * 1024),
|
|
536
|
+
) * 1024 * 1024
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
detected_total_memory_bytes = (
|
|
540
|
+
total_memory_bytes if total_memory_bytes is not None else _total_memory_bytes()
|
|
541
|
+
)
|
|
542
|
+
detected_available_memory_bytes = (
|
|
543
|
+
available_memory_bytes
|
|
544
|
+
if available_memory_bytes is not None
|
|
545
|
+
else _available_memory_bytes()
|
|
546
|
+
)
|
|
547
|
+
memory_reserve_bytes = None
|
|
548
|
+
memory_bound = None
|
|
549
|
+
if detected_available_memory_bytes is not None:
|
|
550
|
+
total_reserve = (
|
|
551
|
+
detected_total_memory_bytes // 4
|
|
552
|
+
if detected_total_memory_bytes is not None
|
|
553
|
+
else 0
|
|
554
|
+
)
|
|
555
|
+
memory_reserve_bytes = max(AUTO_NUM_THREADS_MEMORY_RESERVE_BYTES, total_reserve)
|
|
556
|
+
usable_memory_bytes = max(0, detected_available_memory_bytes - memory_reserve_bytes)
|
|
557
|
+
memory_bound = max(1, usable_memory_bytes // max(1, resolved_worker_memory_bytes))
|
|
558
|
+
|
|
559
|
+
detected_fd_limit = fd_limit if fd_limit is not None else _open_file_soft_limit()
|
|
560
|
+
fd_bound = None
|
|
561
|
+
if detected_fd_limit is not None:
|
|
562
|
+
usable_fds = max(0, detected_fd_limit - AUTO_NUM_THREADS_FD_HEADROOM)
|
|
563
|
+
fd_bound = max(1, usable_fds // AUTO_NUM_THREADS_FDS_PER_WORKER)
|
|
564
|
+
|
|
565
|
+
if parsed != "auto":
|
|
566
|
+
return NumThreadsPlan(
|
|
567
|
+
requested=requested,
|
|
568
|
+
resolved=parsed,
|
|
569
|
+
row_bound=row_bound,
|
|
570
|
+
cpu_bound=cpu_bound,
|
|
571
|
+
memory_bound=memory_bound,
|
|
572
|
+
fd_bound=fd_bound,
|
|
573
|
+
hard_cap=resolved_hard_cap,
|
|
574
|
+
worker_memory_bytes=resolved_worker_memory_bytes,
|
|
575
|
+
available_memory_bytes=detected_available_memory_bytes,
|
|
576
|
+
total_memory_bytes=detected_total_memory_bytes,
|
|
577
|
+
memory_reserve_bytes=memory_reserve_bytes,
|
|
578
|
+
limiting_factor="explicit",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
limits = {
|
|
582
|
+
"rows": row_bound,
|
|
583
|
+
"cpu": cpu_bound,
|
|
584
|
+
"hard_cap": resolved_hard_cap,
|
|
585
|
+
}
|
|
586
|
+
if memory_bound is not None:
|
|
587
|
+
limits["memory"] = memory_bound
|
|
588
|
+
if fd_bound is not None:
|
|
589
|
+
limits["file_descriptors"] = fd_bound
|
|
590
|
+
resolved = max(1, min(limits.values()))
|
|
591
|
+
limiting_factor = next(
|
|
592
|
+
name
|
|
593
|
+
for name in ("rows", "cpu", "memory", "file_descriptors", "hard_cap")
|
|
594
|
+
if limits.get(name) == resolved
|
|
595
|
+
)
|
|
596
|
+
return NumThreadsPlan(
|
|
597
|
+
requested=requested,
|
|
598
|
+
resolved=resolved,
|
|
599
|
+
row_bound=row_bound,
|
|
600
|
+
cpu_bound=cpu_bound,
|
|
601
|
+
memory_bound=memory_bound,
|
|
602
|
+
fd_bound=fd_bound,
|
|
603
|
+
hard_cap=resolved_hard_cap,
|
|
604
|
+
worker_memory_bytes=resolved_worker_memory_bytes,
|
|
605
|
+
available_memory_bytes=detected_available_memory_bytes,
|
|
606
|
+
total_memory_bytes=detected_total_memory_bytes,
|
|
607
|
+
memory_reserve_bytes=memory_reserve_bytes,
|
|
608
|
+
limiting_factor=limiting_factor,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
319
612
|
def _is_better(challenger: float, incumbent: float, higher_is_better: bool) -> bool:
|
|
320
613
|
return challenger > incumbent if higher_is_better else challenger < incumbent
|
|
321
614
|
|
|
@@ -564,12 +857,13 @@ def evaluate_candidate(
|
|
|
564
857
|
scorer_runner: ScorerRunner,
|
|
565
858
|
budget: Budget,
|
|
566
859
|
evaluation_cache: EvaluationCache | None = None,
|
|
860
|
+
num_threads: int = 1,
|
|
567
861
|
) -> list[RowEvaluation]:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
break
|
|
862
|
+
"""Evaluate one candidate over rows, preserving row order while parallelizing uncached calls."""
|
|
863
|
+
evaluations_by_index: dict[int, RowEvaluation] = {}
|
|
864
|
+
pending: list[tuple[int, DatasetRow, str | None]] = []
|
|
572
865
|
|
|
866
|
+
for index, row in enumerate(rows):
|
|
573
867
|
cache_key = None
|
|
574
868
|
if evaluation_cache is not None:
|
|
575
869
|
cache_key = evaluation_cache.key(
|
|
@@ -581,9 +875,12 @@ def evaluate_candidate(
|
|
|
581
875
|
)
|
|
582
876
|
cached = evaluation_cache.get(cache_key)
|
|
583
877
|
if cached is not None:
|
|
584
|
-
|
|
878
|
+
evaluations_by_index[index] = cached
|
|
585
879
|
continue
|
|
586
880
|
|
|
881
|
+
pending.append((index, row, cache_key))
|
|
882
|
+
|
|
883
|
+
def run_uncached_row(index: int, row: DatasetRow, cache_key: str | None) -> tuple[int, str | None, RowEvaluation]:
|
|
587
884
|
started = time.time()
|
|
588
885
|
candidate_result = candidate_runner(candidate_text, row, prompt_context, candidate_id)
|
|
589
886
|
scorer_result = scorer_runner(row, candidate_result, scorer_context, candidate_id)
|
|
@@ -610,11 +907,57 @@ def evaluate_candidate(
|
|
|
610
907
|
if candidate_result.cost_usd is not None or scorer_result.cost_usd is not None else None,
|
|
611
908
|
error=candidate_result.error or scorer_result.error,
|
|
612
909
|
)
|
|
613
|
-
|
|
910
|
+
return index, cache_key, evaluation
|
|
911
|
+
|
|
912
|
+
worker_count = max(1, min(max(1, num_threads), len(pending) or 1))
|
|
913
|
+
if worker_count == 1:
|
|
914
|
+
completed = [run_uncached_row(index, row, cache_key) for index, row, cache_key in pending]
|
|
915
|
+
else:
|
|
916
|
+
completed = []
|
|
917
|
+
next_pending_index = 0
|
|
918
|
+
executor = ThreadPoolExecutor(max_workers=worker_count)
|
|
919
|
+
futures = set()
|
|
920
|
+
|
|
921
|
+
def submit_next_pending() -> bool:
|
|
922
|
+
nonlocal next_pending_index
|
|
923
|
+
if next_pending_index >= len(pending):
|
|
924
|
+
return False
|
|
925
|
+
index, row, cache_key = pending[next_pending_index]
|
|
926
|
+
next_pending_index += 1
|
|
927
|
+
futures.add(executor.submit(run_uncached_row, index, row, cache_key))
|
|
928
|
+
return True
|
|
929
|
+
|
|
930
|
+
try:
|
|
931
|
+
for _ in range(worker_count):
|
|
932
|
+
if not submit_next_pending():
|
|
933
|
+
break
|
|
934
|
+
while futures:
|
|
935
|
+
done, _ = wait(futures, return_when=FIRST_COMPLETED)
|
|
936
|
+
batch = []
|
|
937
|
+
for future in done:
|
|
938
|
+
futures.remove(future)
|
|
939
|
+
batch.append(future.result())
|
|
940
|
+
completed.extend(batch)
|
|
941
|
+
for _ in batch:
|
|
942
|
+
submit_next_pending()
|
|
943
|
+
except BaseException:
|
|
944
|
+
for future in futures:
|
|
945
|
+
future.cancel()
|
|
946
|
+
executor.shutdown(wait=True)
|
|
947
|
+
raise
|
|
948
|
+
else:
|
|
949
|
+
executor.shutdown(wait=True)
|
|
950
|
+
|
|
951
|
+
for index, cache_key, evaluation in completed:
|
|
952
|
+
evaluations_by_index[index] = evaluation
|
|
614
953
|
if evaluation_cache is not None and cache_key is not None:
|
|
615
954
|
evaluation_cache.set(cache_key, evaluation)
|
|
616
955
|
budget.used_metric_calls += 1
|
|
617
|
-
|
|
956
|
+
|
|
957
|
+
return [
|
|
958
|
+
evaluations_by_index[index]
|
|
959
|
+
for index in sorted(evaluations_by_index)
|
|
960
|
+
]
|
|
618
961
|
|
|
619
962
|
|
|
620
963
|
def _completed_all_rows(results: list[RowEvaluation], rows: list[DatasetRow]) -> bool:
|
|
@@ -636,7 +979,17 @@ def optimize_loaded_text_candidate(
|
|
|
636
979
|
local_logger: Any | None = None,
|
|
637
980
|
) -> TextGepaResult:
|
|
638
981
|
seed_text = prompt_context.body or ""
|
|
639
|
-
budget = Budget.from_config(
|
|
982
|
+
budget = Budget.from_config(
|
|
983
|
+
config,
|
|
984
|
+
trainset_size=len(trainset),
|
|
985
|
+
valset_size=len(valset),
|
|
986
|
+
num_components=1,
|
|
987
|
+
)
|
|
988
|
+
num_threads_plan = resolve_num_threads(
|
|
989
|
+
config.num_threads,
|
|
990
|
+
minibatch_size=config.minibatch_size,
|
|
991
|
+
validation_count=len(valset),
|
|
992
|
+
)
|
|
640
993
|
evaluation_cache = EvaluationCache() if config.cache_evaluations else None
|
|
641
994
|
candidate_text_by_id = {"seed": seed_text}
|
|
642
995
|
val_scores_by_candidate: dict[str, dict[str, float]] = {}
|
|
@@ -664,6 +1017,7 @@ def optimize_loaded_text_candidate(
|
|
|
664
1017
|
"train_count": len(trainset),
|
|
665
1018
|
"validation_count": len(valset),
|
|
666
1019
|
"config": dataclasses.asdict(config),
|
|
1020
|
+
"num_threads": num_threads_plan.to_payload(),
|
|
667
1021
|
"budget": budget.to_payload(),
|
|
668
1022
|
})
|
|
669
1023
|
|
|
@@ -684,6 +1038,7 @@ def optimize_loaded_text_candidate(
|
|
|
684
1038
|
scorer_runner=scorer_runner,
|
|
685
1039
|
budget=budget,
|
|
686
1040
|
evaluation_cache=evaluation_cache,
|
|
1041
|
+
num_threads=num_threads_plan.resolved,
|
|
687
1042
|
)
|
|
688
1043
|
if local_logger is not None:
|
|
689
1044
|
local_logger.append_evaluations(
|
|
@@ -694,7 +1049,7 @@ def optimize_loaded_text_candidate(
|
|
|
694
1049
|
results=seed_results,
|
|
695
1050
|
)
|
|
696
1051
|
if not _completed_all_rows(seed_results, valset):
|
|
697
|
-
raise RuntimeError("
|
|
1052
|
+
raise RuntimeError("Seed validation did not complete all requested rows")
|
|
698
1053
|
budget.used_full_evals += 1
|
|
699
1054
|
seed_score = _mean(seed_results)
|
|
700
1055
|
val_scores_by_candidate["seed"] = {result.row_id: result.score for result in seed_results}
|
|
@@ -725,6 +1080,20 @@ def optimize_loaded_text_candidate(
|
|
|
725
1080
|
candidate_id=best_candidate_id,
|
|
726
1081
|
)
|
|
727
1082
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension")
|
|
1083
|
+
if not budget.allows_iteration():
|
|
1084
|
+
event_sink.log_event(
|
|
1085
|
+
"budget_exhausted",
|
|
1086
|
+
{
|
|
1087
|
+
"stage": "seed_val_set_completed",
|
|
1088
|
+
"requested_rows": len(valset),
|
|
1089
|
+
"completed_rows": len(seed_results),
|
|
1090
|
+
"budget": budget.to_payload(),
|
|
1091
|
+
"decision_rule": "budget is checked only after completing seed validation or a full iteration",
|
|
1092
|
+
},
|
|
1093
|
+
event_layer="extension",
|
|
1094
|
+
candidate_id="seed",
|
|
1095
|
+
)
|
|
1096
|
+
budget_exhausted = True
|
|
728
1097
|
|
|
729
1098
|
for iteration in range(1, config.max_iterations + 1):
|
|
730
1099
|
if not budget.allows_iteration():
|
|
@@ -787,6 +1156,7 @@ def optimize_loaded_text_candidate(
|
|
|
787
1156
|
scorer_runner=scorer_runner,
|
|
788
1157
|
budget=budget,
|
|
789
1158
|
evaluation_cache=evaluation_cache,
|
|
1159
|
+
num_threads=num_threads_plan.resolved,
|
|
790
1160
|
)
|
|
791
1161
|
if local_logger is not None:
|
|
792
1162
|
local_logger.append_evaluations(
|
|
@@ -863,6 +1233,21 @@ def optimize_loaded_text_candidate(
|
|
|
863
1233
|
iteration=iteration,
|
|
864
1234
|
candidate_id=best_candidate_id,
|
|
865
1235
|
)
|
|
1236
|
+
if iteration < config.max_iterations and not budget.allows_iteration():
|
|
1237
|
+
event_sink.log_event(
|
|
1238
|
+
"budget_exhausted",
|
|
1239
|
+
{
|
|
1240
|
+
"stage": "iteration_completed",
|
|
1241
|
+
"completed_iteration": iteration,
|
|
1242
|
+
"budget": budget.to_payload(),
|
|
1243
|
+
"decision_rule": "budget is checked only between iterations",
|
|
1244
|
+
},
|
|
1245
|
+
event_layer="extension",
|
|
1246
|
+
iteration=iteration,
|
|
1247
|
+
candidate_id=best_candidate_id,
|
|
1248
|
+
)
|
|
1249
|
+
budget_exhausted = True
|
|
1250
|
+
break
|
|
866
1251
|
continue
|
|
867
1252
|
|
|
868
1253
|
event_sink.log_event(
|
|
@@ -946,6 +1331,7 @@ def optimize_loaded_text_candidate(
|
|
|
946
1331
|
scorer_runner=scorer_runner,
|
|
947
1332
|
budget=budget,
|
|
948
1333
|
evaluation_cache=evaluation_cache,
|
|
1334
|
+
num_threads=num_threads_plan.resolved,
|
|
949
1335
|
)
|
|
950
1336
|
if local_logger is not None:
|
|
951
1337
|
local_logger.append_evaluations(
|
|
@@ -1028,6 +1414,7 @@ def optimize_loaded_text_candidate(
|
|
|
1028
1414
|
scorer_runner=scorer_runner,
|
|
1029
1415
|
budget=budget,
|
|
1030
1416
|
evaluation_cache=evaluation_cache,
|
|
1417
|
+
num_threads=num_threads_plan.resolved,
|
|
1031
1418
|
)
|
|
1032
1419
|
if local_logger is not None:
|
|
1033
1420
|
local_logger.append_evaluations(
|
|
@@ -1152,6 +1539,21 @@ def optimize_loaded_text_candidate(
|
|
|
1152
1539
|
iteration=iteration,
|
|
1153
1540
|
candidate_id=best_candidate_id,
|
|
1154
1541
|
)
|
|
1542
|
+
if iteration < config.max_iterations and not budget.allows_iteration():
|
|
1543
|
+
event_sink.log_event(
|
|
1544
|
+
"budget_exhausted",
|
|
1545
|
+
{
|
|
1546
|
+
"stage": "iteration_completed",
|
|
1547
|
+
"completed_iteration": iteration,
|
|
1548
|
+
"budget": budget.to_payload(),
|
|
1549
|
+
"decision_rule": "budget is checked only between iterations",
|
|
1550
|
+
},
|
|
1551
|
+
event_layer="extension",
|
|
1552
|
+
iteration=iteration,
|
|
1553
|
+
candidate_id=best_candidate_id,
|
|
1554
|
+
)
|
|
1555
|
+
budget_exhausted = True
|
|
1556
|
+
break
|
|
1155
1557
|
|
|
1156
1558
|
if budget_exhausted:
|
|
1157
1559
|
event_sink.log_event(
|
|
@@ -148,6 +148,7 @@ Bundled `run-gepa` reflection behavior:
|
|
|
148
148
|
- Reflection max-token limits are explicit config, not implicit defaults. Use `--reflection-max-tokens <n>` when a provider requires an output cap; this maps to Anthropic `max_tokens` and OpenAI `max_output_tokens`.
|
|
149
149
|
- Anthropic native Messages reflection requires `max_tokens`, so `run-gepa` with an `anthropic/...` reflection model must receive `--reflection-max-tokens <n>` and should fail fast if it is omitted. OpenAI reflection may omit `max_output_tokens` unless the user explicitly wants a cap.
|
|
150
150
|
- By default, `run-gepa` skips reflection and child creation when every row in the selected parent mini-batch is already perfect (`1.0` for higher-is-better scorers, `0.0` for lower-is-better scorers). This saves reflection tokens because the child cannot beat a saturated mini-batch. Use `--no-skip-perfect-parent-reflection` to force reflection anyway, or `--skip-perfect-parent-reflection` to make the default explicit in scripted runs.
|
|
151
|
+
- By default, `run-gepa` sets `--num-threads auto` for row evaluations. Auto uses mini-batch size, validation-set size, CPU count, memory estimate, file-descriptor limit, and an 8-thread hard cap to avoid launching too many runner subprocesses at once. Use `--num-threads <n>` only when the runner/provider capacity is known.
|
|
151
152
|
- `run-gepa` writes complete local logs by default under `logs/<optimization_run_id>`; override with `--log-dir <dir>` or disable with `--no-local-log`.
|
|
152
153
|
- Server optimization events redact row snapshots and reflection prompts by default; the local log keeps full row inputs, outputs, scores, feedback, reflection prompts, and reflection responses for later agent analysis.
|
|
153
154
|
- OpenAI example: `--reflection-model openai/gpt-5 --reflection-provider-settings '{"reasoning":{"effort":"medium","summary":"auto"}}'`.
|
|
@@ -232,12 +232,13 @@ For prompts, judges, runners, run submission, optimizer artifacts, live event lo
|
|
|
232
232
|
Optimization trace commands:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
orizu optimizations run-gepa ... [--log-dir logs] [--no-local-log]
|
|
235
|
+
orizu optimizations run-gepa ... [--num-threads auto|N] [--log-dir logs] [--no-local-log]
|
|
236
236
|
orizu optimizations export <optimization-run-id> --out ./optimization.json
|
|
237
237
|
orizu optimizations export <optimization-run-id> --json
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
Behavior:
|
|
241
|
+
- `run-gepa` defaults `--num-threads` to `auto`, resolving a conservative row-evaluation parallelism cap from mini-batch size, validation-set size, CPU count, memory estimate, file-descriptor limit, and an 8-thread hard cap.
|
|
241
242
|
- `run-gepa` writes a complete local trace under `logs/<optimization_run_id>` by default.
|
|
242
243
|
- The local trace is the best artifact for coding-agent analysis because it includes full rows, outputs, scores, feedback, scorer responses, reflection prompts, reflection responses, candidate text, and `result.json`.
|
|
243
244
|
- `optimizations export` writes a portable JSON artifact from server data when the local log is unavailable or the run happened elsewhere.
|
|
@@ -84,7 +84,7 @@ Report:
|
|
|
84
84
|
- Acceptance rate by iteration. Many rejections suggest reflection is exploring but not finding hills; many acceptances suggest it is climbing a real signal.
|
|
85
85
|
- Budget state. If budget was exhausted, the run paused before auto-promotion; manually decide whether to promote/finish. If max iterations was reached with budget remaining, the run likely converged under the configured search.
|
|
86
86
|
- `scoreOverTime` / validation trajectory. Still climbing at the end suggests raising budget or iterations. Plateaued curves point toward scorer, feedback, or data work upstream.
|
|
87
|
-
- Reflection model, inference model, scorer model, provider settings, minibatch size, budget preset/limit, candidate selection strategy, seed, and cache settings.
|
|
87
|
+
- Reflection model, inference model, scorer model, provider settings, minibatch size, resolved thread count, budget preset/limit, candidate selection strategy, seed, and cache settings.
|
|
88
88
|
|
|
89
89
|
### 5. Recommendations
|
|
90
90
|
|
|
@@ -130,9 +130,9 @@ Set an explicit expectation before the run: for example, "if best kappa does not
|
|
|
130
130
|
|
|
131
131
|
### Budget Versus Iteration Cap
|
|
132
132
|
|
|
133
|
-
Default budget presets are metric-call budgets
|
|
133
|
+
Default budget presets are DSPy-style metric-call budgets. `light`, `medium`, and `heavy` map to DSPy's `auto="light"`, `auto="medium"`, and `auto="heavy"` scales; `auto` is treated as the balanced `medium` preset. The final metric-call limit scales with validation-set size, rather than using fixed tiny constants. `--max-metric-calls` overrides this directly, while `--max-full-evals` is converted to a metric-call budget over `train + validation` rows.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Budget is checked only between iterations. Once an iteration starts, `run-gepa` completes the parent minibatch, child minibatch, and full validation eval for an accepted child even if that overshoots the nominal budget. If a run ends as `budget_exhausted`, it pauses and does not auto-promote. If auto-promotion matters, either increase budget to fit the iteration target or lower `--max-iterations` to fit the budget.
|
|
136
136
|
|
|
137
137
|
### Reflection Signal Quality
|
|
138
138
|
|
|
@@ -609,13 +609,15 @@ orizu --local optimizations run-gepa \
|
|
|
609
609
|
--log-dir ./logs \
|
|
610
610
|
--budget light \
|
|
611
611
|
--max-iterations 3 \
|
|
612
|
-
--minibatch-size 3
|
|
612
|
+
--minibatch-size 3 \
|
|
613
|
+
--num-threads auto
|
|
613
614
|
```
|
|
614
615
|
|
|
615
616
|
Useful GEPA flags:
|
|
616
617
|
|
|
617
|
-
- `--budget auto|light|medium|
|
|
618
|
+
- `--budget auto|light|medium|heavy`, `--max-metric-calls <n>`, `--max-full-evals <n>`.
|
|
618
619
|
- `--minibatch-size <n>` defaults to 3.
|
|
620
|
+
- `--num-threads auto|N` defaults to `auto`; auto caps row-evaluation concurrency from mini-batch size, validation-set size, CPU count, memory estimate, file-descriptor limit, and an 8-thread hard cap.
|
|
619
621
|
- `--candidate-selection-strategy pareto|current_best|epsilon_greedy`; default is `pareto`.
|
|
620
622
|
- `--reflection-model <provider/model>`, `--reflection-temperature <n>`, `--reflection-prompt-template <text|@file>`.
|
|
621
623
|
- `--reflection-max-tokens <n>` is explicit provider config, not a global default. It maps to Anthropic `max_tokens` and OpenAI `max_output_tokens`; Anthropic native Messages reflection requires it, while OpenAI can omit it unless the user wants a cap.
|