refactor: batch-based parallel proposals (replaces #314's threading)#330
refactor: batch-based parallel proposals (replaces #314's threading)#330LakshyAAAgrawal wants to merge 9 commits into
Conversation
Remove ThreadPoolExecutor-based parallelism from the engine and proposer. Restore ReflectiveMutationProposer.propose() as a single monolithic method, remove ProposalContext/ProposalOutput dataclasses, threading lock, and the split prepare/execute/apply methods. Remove num_parallel_proposals from engine and EngineConfig. The engine now calls propose() directly. This prepares for a batch-based parallel proposals design where parallelism is delegated to adapter.batch_evaluate() and LM.batch_complete() instead of threading in GEPA's core. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ator Add default_batch_evaluate() standalone function to adapter.py that sequentially calls adapter.evaluate() for each (candidate, batch) pair. Document optional batch_evaluate method on GEPAAdapter protocol. Add batch_evaluator parameter to OptimizeAnythingAdapter and optimize_anything() — when provided, all (candidate, example) pairs are flattened into a single call to the user's batch function for true batching. Incorporates the batch_evaluator concept from PR #279. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce a new parallel proposals system that delegates all parallelism to adapter.batch_evaluate() and LM.batch_complete() — no ThreadPoolExecutor in GEPA's core. The engine stays sequential and async-ready. New file src/gepa/proposer/parallel.py with: - ParallelSamplingStrategy protocol + built-in strategies: SingleMutationSampling, SameParentSampling(n), IndependentSampling(n), PxNSampling(p, n) - ProposalSelectionStrategy protocol + built-in strategies: AllImprovements, BestImprovement, TopKImprovements(k) - propose_batch() 5-stage pipeline: sample -> batch eval parents (deduplicated) -> reflect per task -> batch eval children -> select Wire ParallelConfig into GEPAEngine, api.optimize(), and optimize_anything() EngineConfig. When parallel_config is set, the engine uses propose_batch(); otherwise the default sequential path is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed Files
|
Convert result to list[Any] before indexing to avoid pyright narrowing tuple type to tuple[()] in the else branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| # Signature: | ||
| # batch_evaluate( | ||
| # self, | ||
| # items: list[tuple[dict[str, str], list[DataInst]]], |
There was a problem hiding this comment.
Why is the signature so convoluted?
It should simply be list[tuple[Candidate, DataInst]]?
Address review comment: use the existing Candidate type alias (dict[str, str]) in batch_evaluate signatures instead of spelling out dict[str, str] everywhere. Also fix UP038 ruff warning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| Calls ``adapter.evaluate()`` once per (candidate, batch) pair. | ||
| Adapters can implement ``batch_evaluate()`` directly for true batching. | ||
| """ | ||
| return [adapter.evaluate(batch, candidate, capture_traces=capture_traces) for candidate, batch in items] |
There was a problem hiding this comment.
Why just sequential? We already have a threading based parallel implementation right (the one in optimize_anything, we can just share it here)?
| # self, | ||
| # items: list[tuple[Candidate, list[DataInst]]], | ||
| # capture_traces: bool = False, | ||
| # ) -> list[EvaluationBatch[Trajectory, RolloutOutput]] |
There was a problem hiding this comment.
Why is this not a field, with a protocol like propose_new_texts above?
| output = self.reflective_proposer.propose_output(state) | ||
| proposal_accepted = self._process_proposal_output( | ||
| output, state.i + 1, state.full_program_trace[-1], state | ||
| if self.parallel_config is not None: |
There was a problem hiding this comment.
Why are there 2 separate paths? When parallel config is not provided (default behaviour), we should have a simple parallel config that simply proposes a batch of size one? Shouldn't that be the case?
| # --- Built-in sampling strategies --- | ||
|
|
||
|
|
||
| class SingleMutationSampling: |
There was a problem hiding this comment.
Why aren't these mapped to the protocol they implement?
| @@ -0,0 +1,343 @@ | |||
| # Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors | |||
There was a problem hiding this comment.
Ideally, this shouldn't be a separate "parallel" file? This is just a normal part of GEPA, where earlier reflective mutation was happening on one parent with one minibatch, instead now it can happen on multiple parents with their respective minibatches. The default behaviour (one parent, one minibatch) should just be a special case, and shouldn't require a separate code path? Let's try to build code concepts that minimize the number of new code branches/paths, and implement general concepts that follow good coding patterns.
Address PR review: remove the dual code path (parallel_config vs sequential) and make batch-based proposals the ONLY path, with SingleMutationSampling (n=1) as the default. Key changes: - propose() now returns list[CandidateProposal] (empty = nothing proposed) - batch_evaluate is a proper protocol field on GEPAAdapter (like propose_new_texts) - Sampling/selection strategies moved to strategies/ (proposal_sampling.py, proposal_selection.py) alongside other strategy modules - Engine has ONE code path: always calls propose(), iterates over results - Delete proposer/parallel.py — no separate "parallel" concept - Remove ParallelConfig — strategies are set directly on the proposer Default behavior is identical: SingleMutationSampling produces 1 task, AllImprovements passes any improvement, matching the old sequential flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert ProposeNewCandidate protocol to return CandidateProposal | None (MergeProposer depends on this signature) - Keep batch_evaluate as a documented optional method (comment-based) rather than a protocol field to avoid override type conflict - Restore uv.lock from main Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
||
| def __call__( | ||
| self, | ||
| items: list[tuple[Candidate, list]], |
There was a problem hiding this comment.
Why is the type a list of tuple[Candidate, list]? Why can't we keep it simply a tuple[Candidate, Data]?
Summary
Commit 1: Reverts Parallel Proposals #314's
ThreadPoolExecutor-based parallelism from the engine and proposer. RestoresReflectiveMutationProposer.propose()as a single monolithic method, removesProposalContext/ProposalOutputdataclasses, threading lock, andnum_parallel_proposalsfrom engine/EngineConfig.Commit 2: Adds
batch_evaluateto theGEPAAdapterprotocol — adapters can optionally implementbatch_evaluate(items, capture_traces)for true batching. Includesdefault_batch_evaluate()sequential fallback. Wiresbatch_evaluatorparam throughoptimize_anything()→OptimizeAnythingAdapter(incorporates feat: batch_evaluator parameter for optimize_anything #279's concept).Commit 3: Introduces batch-based parallel proposals in
src/gepa/proposer/parallel.py:SingleMutationSampling,SameParentSampling(n),IndependentSampling(n),PxNSampling(p, n)AllImprovements,BestImprovement,TopKImprovements(k)propose_batch()5-stage pipeline: sample → batch eval parents (deduplicated) → reflect per task → batch eval children → selectThreadPoolExecutor— all parallelism delegated toadapter.batch_evaluate()andLM.batch_complete()ParallelConfigintoGEPAEngine,api.optimize(), andoptimize_anything()EngineConfigDesign rationale
The engine stays sequential and async-ready. Parallelism is pushed to the edges: adapters batch their evaluations, LMs batch their completions. This avoids thread-safety concerns in GEPA's core state management and makes the architecture compatible with future async support (#329).
Test plan
tests/test_parallel_proposals.pycovering:default_batch_evaluatesequential fallbackpropose_batchwith no tasks, no trajectories, improvement, and deduplicationCloses #314 (superseded by this approach).
Incorporates #279 (batch_evaluator concept).
🤖 Generated with Claude Code