Repository navigation
feat(gepa): parallelize candidate evaluation - #10210
Conversation
Greptile SummaryGEPA now evaluates program variations concurrently and divides the configured thread budget across them, including any remainder. Execution checks confirmed that the complete budget is used, result order is preserved despite out-of-order completion, and caller context, tracing, and callback identity remain available to every worker. The existing legacy language-model cost-accounting problem remains: bridged typed calls retain a per-response cost but do not maintain cumulative Confidence Score: 4/5The GEPA concurrency change behaved correctly in exercised allocation, ordering, context, tracing, and callback flows, but legacy typed-bridge calls still lack cumulative cost tracking. One actionable non-security finding remains. isaacbmiller stated that the issue was fixed, but the current code has no Files Needing Attention: dspy/clients/base_lm.py
What T-Rex did
|
9587315 to
8d00f61
Compare
dfb67f5 to
25357c5
Compare
25357c5 to
ae51258
Compare
Amp-Thread-ID: https://ampcode.com/threads/T-019ffe40-c240-70bb-8cb2-db33146ed8e6 Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
Amp-Thread-ID: https://ampcode.com/threads/T-019ffe40-c240-70bb-8cb2-db33146ed8e6 Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
ae51258 to
71b9bd6
Compare
Why this PR exists
Stacked on #10209, which upgrades DSPy to GEPA 0.1.4 while retaining GEPA's sequential candidate-evaluation fallback.
GEPA 0.1.4 can sample multiple proposals in one iteration, but leaves execution policy to
adapter.batch_evaluate(). Without that method, DSPy evaluates candidates one after another. This PR implements that adapter capability while preserving DSPy's normal meaning ofnum_threadsas the total evaluation budget.Before and after
num_threadsnum_threadsnum_threadsnum_threadsmax_reflection_costCandidate and example concurrency do not multiply. Four candidates with
num_threads=8receive two example workers each; one candidate receives all eight. If candidates outnumber threads, at most eight run at once with one worker each.With GEPA's default one-proposal strategy, execution remains equivalent to ordinary DSPy evaluation. Strategies such as
IndependentSampling(4)orPxNSampling(2, 2)cause DSPy to partition the existing budget across candidates.Design
DspyAdapter.batch_evaluate():(candidate, batch)items from GEPA;num_threadssetting;min(number_of_candidates, num_threads)candidates concurrently;ContextVarstate and callback ancestry;capture_traces; andIt uses a small
ThreadPoolExecutorrather than DSPy'sParallelExecutor.ParallelExecutorcatches failures and may resubmit stragglers; at this boundary, resubmission could duplicate paid LM calls.Deliberately not included: reflection-cost accounting
GEPA's
max_reflection_costexpects cumulativetotal_cost. DSPy's LM history records response cost, but history can be disabled, bounded/evicted, cleared, or replaced. Summing retained history would be unreliable, while adding a mutable_total_costwould create a second source of truth plus locking and serialization behavior.This PR does neither. Non-
Nonemax_reflection_costremains explicitly rejected. Cost budgeting should use a coherent DSPy-wide accounting design rather than GEPA-specific LM state.User example
This samples four independent proposals, evaluates them concurrently within eight total threads, retains the best improvement, and supports checkpoint resume through
log_dir.Measured behavior
A controlled end-to-end GEPA 0.1.4 benchmark compared this branch with #10209. Both used
num_threads=4for one completeIndependentSampling(4)iteration with four distinct improvements,AllImprovements, and an 80 ms simulated candidate evaluation:That is a 3.16× speedup with identical work/results, consistent with GEPA's advertised “up to about 3 to 4×” result. It is a controlled latency benchmark, not a guarantee or a reproduction of GEPA's quality benchmarks.
A second probe used the real DSPy
Evaluatepath with four candidates × eight examples andnum_threads=8. Peak simultaneous example evaluations remained exactly 8, proving this does not create 32 workers. All four ordered eight-example results were retained.Risks and compatibility
Scheduling shape
More candidate program instances can be active concurrently, while each receives fewer example workers. Total DSPy-controlled concurrency remains bounded by
num_threads. Uneven candidate durations may change utilization, but users do not need a second concurrency knob.GEPA 0.1.4 → 0.1.5
GEPA 0.1.4 calls
batch_evaluate(items)and always requests trajectories, including accepted-candidate full-validation evaluation. That is gepa-ai/gepa#428.This adapter accepts the trace-aware contract expected from gepa-ai/gepa#430:
batch_evaluate(items, *, capture_traces=...). Under that change, reflection requests traces while seed/full-validation evaluation do not. Against 0.1.4, the keyword is omitted and DSPy's default remainsTrue, so the known limitation remains. Updating to 0.1.5 will activate correct routing without another adapter redesign; no temporary monkeypatch is included.Review guide
The complete diff against #10209 is three files:
dspy/teleprompt/gepa/gepa_utils.py: candidate batching and thread-budget partitioningdspy/teleprompt/gepa/gepa.py: documents sharednum_threadssemanticstests/teleprompt/test_gepa.py: overlap, ordering, context, trace forwarding, and 2+2 / 4 budget allocationThere are no new public concurrency parameters and no
BaseLM, history, cost, lock, copy, or serialization changes.Verification
uv run pytest tests/teleprompt -q— 82 passeduv run pytest -q tests/clients/test_lm.py tests/teleprompt/test_gepa.py— 108 passeduv run pre-commit run --all-files— passeduv lock --check— passedgit diff --check— passed