orizu 0.3.5 → 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/README.md +25 -3
- package/dist/help.js +611 -0
- package/dist/index.js +268 -15
- package/dist/skill-installer.js +180 -0
- package/package.json +3 -3
- package/scripts/vendor-orizu-skill.mjs +54 -0
- package/src/help.ts +676 -0
- package/src/index.ts +353 -15
- package/src/skill-installer.ts +260 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/__init__.py +4 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +24 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +469 -21
- package/vendor/skills/orizu-cli/SKILL.md +186 -0
- package/vendor/skills/orizu-cli/agents/openai.yaml +4 -0
- package/vendor/skills/orizu-cli/references/building-apps.md +1461 -0
- package/vendor/skills/orizu-cli/references/building-judges.md +218 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +325 -0
- package/vendor/skills/orizu-cli/references/optimization-reports.md +174 -0
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +291 -0
- package/vendor/skills/orizu-cli/references/primer.md +245 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +832 -0
- package/vendor/skills/orizu-cli/scripts/test-app.mjs +492 -0
|
@@ -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
|
|
@@ -189,6 +259,7 @@ class TextGepaConfig:
|
|
|
189
259
|
fail_on_log_error: bool = True
|
|
190
260
|
log_row_snapshots: bool = False
|
|
191
261
|
cache_evaluations: bool = True
|
|
262
|
+
skip_perfect_parent_reflection: bool = True
|
|
192
263
|
|
|
193
264
|
|
|
194
265
|
@dataclass(frozen=True)
|
|
@@ -315,10 +386,241 @@ def _metric_key(scorer_context: PromptContext) -> str:
|
|
|
315
386
|
return scorer_context.metric_key or "score"
|
|
316
387
|
|
|
317
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
|
+
|
|
318
612
|
def _is_better(challenger: float, incumbent: float, higher_is_better: bool) -> bool:
|
|
319
613
|
return challenger > incumbent if higher_is_better else challenger < incumbent
|
|
320
614
|
|
|
321
615
|
|
|
616
|
+
def _is_perfect_minibatch(results: list[RowEvaluation], higher_is_better: bool) -> bool:
|
|
617
|
+
if not results:
|
|
618
|
+
return False
|
|
619
|
+
if higher_is_better:
|
|
620
|
+
return all(result.score >= 1.0 for result in results)
|
|
621
|
+
return all(result.score <= 0.0 for result in results)
|
|
622
|
+
|
|
623
|
+
|
|
322
624
|
class EvaluationCache:
|
|
323
625
|
def __init__(self):
|
|
324
626
|
self._values: dict[str, RowEvaluation] = {}
|
|
@@ -555,12 +857,13 @@ def evaluate_candidate(
|
|
|
555
857
|
scorer_runner: ScorerRunner,
|
|
556
858
|
budget: Budget,
|
|
557
859
|
evaluation_cache: EvaluationCache | None = None,
|
|
860
|
+
num_threads: int = 1,
|
|
558
861
|
) -> list[RowEvaluation]:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
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]] = []
|
|
563
865
|
|
|
866
|
+
for index, row in enumerate(rows):
|
|
564
867
|
cache_key = None
|
|
565
868
|
if evaluation_cache is not None:
|
|
566
869
|
cache_key = evaluation_cache.key(
|
|
@@ -572,9 +875,12 @@ def evaluate_candidate(
|
|
|
572
875
|
)
|
|
573
876
|
cached = evaluation_cache.get(cache_key)
|
|
574
877
|
if cached is not None:
|
|
575
|
-
|
|
878
|
+
evaluations_by_index[index] = cached
|
|
576
879
|
continue
|
|
577
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]:
|
|
578
884
|
started = time.time()
|
|
579
885
|
candidate_result = candidate_runner(candidate_text, row, prompt_context, candidate_id)
|
|
580
886
|
scorer_result = scorer_runner(row, candidate_result, scorer_context, candidate_id)
|
|
@@ -601,11 +907,57 @@ def evaluate_candidate(
|
|
|
601
907
|
if candidate_result.cost_usd is not None or scorer_result.cost_usd is not None else None,
|
|
602
908
|
error=candidate_result.error or scorer_result.error,
|
|
603
909
|
)
|
|
604
|
-
|
|
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
|
|
605
953
|
if evaluation_cache is not None and cache_key is not None:
|
|
606
954
|
evaluation_cache.set(cache_key, evaluation)
|
|
607
955
|
budget.used_metric_calls += 1
|
|
608
|
-
|
|
956
|
+
|
|
957
|
+
return [
|
|
958
|
+
evaluations_by_index[index]
|
|
959
|
+
for index in sorted(evaluations_by_index)
|
|
960
|
+
]
|
|
609
961
|
|
|
610
962
|
|
|
611
963
|
def _completed_all_rows(results: list[RowEvaluation], rows: list[DatasetRow]) -> bool:
|
|
@@ -627,7 +979,17 @@ def optimize_loaded_text_candidate(
|
|
|
627
979
|
local_logger: Any | None = None,
|
|
628
980
|
) -> TextGepaResult:
|
|
629
981
|
seed_text = prompt_context.body or ""
|
|
630
|
-
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
|
+
)
|
|
631
993
|
evaluation_cache = EvaluationCache() if config.cache_evaluations else None
|
|
632
994
|
candidate_text_by_id = {"seed": seed_text}
|
|
633
995
|
val_scores_by_candidate: dict[str, dict[str, float]] = {}
|
|
@@ -655,6 +1017,7 @@ def optimize_loaded_text_candidate(
|
|
|
655
1017
|
"train_count": len(trainset),
|
|
656
1018
|
"validation_count": len(valset),
|
|
657
1019
|
"config": dataclasses.asdict(config),
|
|
1020
|
+
"num_threads": num_threads_plan.to_payload(),
|
|
658
1021
|
"budget": budget.to_payload(),
|
|
659
1022
|
})
|
|
660
1023
|
|
|
@@ -675,6 +1038,7 @@ def optimize_loaded_text_candidate(
|
|
|
675
1038
|
scorer_runner=scorer_runner,
|
|
676
1039
|
budget=budget,
|
|
677
1040
|
evaluation_cache=evaluation_cache,
|
|
1041
|
+
num_threads=num_threads_plan.resolved,
|
|
678
1042
|
)
|
|
679
1043
|
if local_logger is not None:
|
|
680
1044
|
local_logger.append_evaluations(
|
|
@@ -685,7 +1049,7 @@ def optimize_loaded_text_candidate(
|
|
|
685
1049
|
results=seed_results,
|
|
686
1050
|
)
|
|
687
1051
|
if not _completed_all_rows(seed_results, valset):
|
|
688
|
-
raise RuntimeError("
|
|
1052
|
+
raise RuntimeError("Seed validation did not complete all requested rows")
|
|
689
1053
|
budget.used_full_evals += 1
|
|
690
1054
|
seed_score = _mean(seed_results)
|
|
691
1055
|
val_scores_by_candidate["seed"] = {result.row_id: result.score for result in seed_results}
|
|
@@ -716,6 +1080,20 @@ def optimize_loaded_text_candidate(
|
|
|
716
1080
|
candidate_id=best_candidate_id,
|
|
717
1081
|
)
|
|
718
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
|
|
719
1097
|
|
|
720
1098
|
for iteration in range(1, config.max_iterations + 1):
|
|
721
1099
|
if not budget.allows_iteration():
|
|
@@ -778,6 +1156,7 @@ def optimize_loaded_text_candidate(
|
|
|
778
1156
|
scorer_runner=scorer_runner,
|
|
779
1157
|
budget=budget,
|
|
780
1158
|
evaluation_cache=evaluation_cache,
|
|
1159
|
+
num_threads=num_threads_plan.resolved,
|
|
781
1160
|
)
|
|
782
1161
|
if local_logger is not None:
|
|
783
1162
|
local_logger.append_evaluations(
|
|
@@ -819,6 +1198,58 @@ def optimize_loaded_text_candidate(
|
|
|
819
1198
|
candidate_id=parent_candidate_id,
|
|
820
1199
|
)
|
|
821
1200
|
|
|
1201
|
+
parent_is_perfect = _is_perfect_minibatch(parent_results, higher_is_better)
|
|
1202
|
+
if config.skip_perfect_parent_reflection and parent_is_perfect:
|
|
1203
|
+
event_sink.log_event(
|
|
1204
|
+
"reflection_skipped",
|
|
1205
|
+
{
|
|
1206
|
+
"reason": "parent_minibatch_perfect",
|
|
1207
|
+
"parent_candidate_id": parent_candidate_id,
|
|
1208
|
+
"parent_score_total": parent_total,
|
|
1209
|
+
"parent_score_mean": _mean(parent_results),
|
|
1210
|
+
"row_ids": [row.id for row in minibatch],
|
|
1211
|
+
"higher_is_better": higher_is_better,
|
|
1212
|
+
"decision_rule": "skip reflection when every parent minibatch row is already perfect",
|
|
1213
|
+
},
|
|
1214
|
+
event_layer="extension",
|
|
1215
|
+
iteration=iteration,
|
|
1216
|
+
candidate_id=parent_candidate_id,
|
|
1217
|
+
)
|
|
1218
|
+
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
1219
|
+
event_sink.log_event(
|
|
1220
|
+
"iteration_completed",
|
|
1221
|
+
{
|
|
1222
|
+
"parent_candidate_id": parent_candidate_id,
|
|
1223
|
+
"child_candidate_id": None,
|
|
1224
|
+
"parent_train_score_total": parent_total,
|
|
1225
|
+
"child_train_score_total": None,
|
|
1226
|
+
"child_validation_score": None,
|
|
1227
|
+
"best_candidate_id": best_candidate_id,
|
|
1228
|
+
"best_validation_score": best_score,
|
|
1229
|
+
"skipped_reflection": True,
|
|
1230
|
+
"skip_reason": "parent_minibatch_perfect",
|
|
1231
|
+
"budget": budget.to_payload(),
|
|
1232
|
+
},
|
|
1233
|
+
iteration=iteration,
|
|
1234
|
+
candidate_id=best_candidate_id,
|
|
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
|
|
1251
|
+
continue
|
|
1252
|
+
|
|
822
1253
|
event_sink.log_event(
|
|
823
1254
|
"reflection_started",
|
|
824
1255
|
{
|
|
@@ -900,6 +1331,7 @@ def optimize_loaded_text_candidate(
|
|
|
900
1331
|
scorer_runner=scorer_runner,
|
|
901
1332
|
budget=budget,
|
|
902
1333
|
evaluation_cache=evaluation_cache,
|
|
1334
|
+
num_threads=num_threads_plan.resolved,
|
|
903
1335
|
)
|
|
904
1336
|
if local_logger is not None:
|
|
905
1337
|
local_logger.append_evaluations(
|
|
@@ -982,6 +1414,7 @@ def optimize_loaded_text_candidate(
|
|
|
982
1414
|
scorer_runner=scorer_runner,
|
|
983
1415
|
budget=budget,
|
|
984
1416
|
evaluation_cache=evaluation_cache,
|
|
1417
|
+
num_threads=num_threads_plan.resolved,
|
|
985
1418
|
)
|
|
986
1419
|
if local_logger is not None:
|
|
987
1420
|
local_logger.append_evaluations(
|
|
@@ -1106,6 +1539,21 @@ def optimize_loaded_text_candidate(
|
|
|
1106
1539
|
iteration=iteration,
|
|
1107
1540
|
candidate_id=best_candidate_id,
|
|
1108
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
|
|
1109
1557
|
|
|
1110
1558
|
if budget_exhausted:
|
|
1111
1559
|
event_sink.log_event(
|