Skip to content

refactor: batch-based parallel proposals (replaces #314's threading)#330

Open
LakshyAAAgrawal wants to merge 9 commits into
mainfrom
refactor/batch-parallel-proposals
Open

refactor: batch-based parallel proposals (replaces #314's threading)#330
LakshyAAAgrawal wants to merge 9 commits into
mainfrom
refactor/batch-parallel-proposals

Conversation

@LakshyAAAgrawal

Copy link
Copy Markdown
Contributor

Summary

  • Commit 1: Reverts Parallel Proposals #314's ThreadPoolExecutor-based parallelism from the engine and proposer. Restores ReflectiveMutationProposer.propose() as a single monolithic method, removes ProposalContext/ProposalOutput dataclasses, threading lock, and num_parallel_proposals from engine/EngineConfig.

  • Commit 2: Adds batch_evaluate to the GEPAAdapter protocol — adapters can optionally implement batch_evaluate(items, capture_traces) for true batching. Includes default_batch_evaluate() sequential fallback. Wires batch_evaluator param through optimize_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:

    • Sampling strategies: SingleMutationSampling, SameParentSampling(n), IndependentSampling(n), PxNSampling(p, n)
    • Selection strategies: AllImprovements, BestImprovement, TopKImprovements(k)
    • propose_batch() 5-stage pipeline: sample → batch eval parents (deduplicated) → reflect per task → batch eval children → select
    • No ThreadPoolExecutor — all parallelism delegated to adapter.batch_evaluate() and LM.batch_complete()
    • Wired via ParallelConfig into GEPAEngine, api.optimize(), and optimize_anything() EngineConfig

Design 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

  • All 460 existing tests pass (0 failures)
  • 14 new tests in tests/test_parallel_proposals.py covering:
    • All 4 sampling strategies
    • All 3 selection strategies
    • default_batch_evaluate sequential fallback
    • propose_batch with no tasks, no trajectories, improvement, and deduplication
  • pyright: 0 errors
  • ruff: no new warnings

Closes #314 (superseded by this approach).
Incorporates #279 (batch_evaluator concept).

🤖 Generated with Claude Code

gepa-bot and others added 3 commits April 12, 2026 00:56
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>
@semanticdiff-com

semanticdiff-com Bot commented Apr 12, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  src/gepa/proposer/reflective_mutation/reflective_mutation.py  33% smaller
  src/gepa/core/engine.py  11% smaller
  src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py  0% smaller
  src/gepa/api.py  0% smaller
  src/gepa/core/adapter.py  0% smaller
  src/gepa/optimize_anything.py  0% smaller
  src/gepa/strategies/proposal_sampling.py  0% smaller
  src/gepa/strategies/proposal_selection.py  0% smaller
  tests/test_parallel_proposals.py  0% smaller
  uv.lock Unsupported file format

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>
Comment thread src/gepa/core/adapter.py Outdated
# Signature:
# batch_evaluate(
# self,
# items: list[tuple[dict[str, str], list[DataInst]]],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the signature so convoluted?

It should simply be list[tuple[Candidate, DataInst]]?

LakshyAAAgrawal and others added 2 commits April 16, 2026 01:51
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>
Comment thread src/gepa/core/adapter.py Outdated
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]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why just sequential? We already have a threading based parallel implementation right (the one in optimize_anything, we can just share it here)?

Comment thread src/gepa/core/adapter.py Outdated
# self,
# items: list[tuple[Candidate, list[DataInst]]],
# capture_traces: bool = False,
# ) -> list[EvaluationBatch[Trajectory, RolloutOutput]]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not a field, with a protocol like propose_new_texts above?

Comment thread src/gepa/core/engine.py Outdated
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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread src/gepa/proposer/parallel.py Outdated
# --- Built-in sampling strategies ---


class SingleMutationSampling:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't these mapped to the protocol they implement?

Comment thread src/gepa/proposer/parallel.py Outdated
@@ -0,0 +1,343 @@
# Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

gepa-bot and others added 3 commits April 16, 2026 02:51
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>
Comment thread src/gepa/core/adapter.py

def __call__(
self,
items: list[tuple[Candidate, list]],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the type a list of tuple[Candidate, list]? Why can't we keep it simply a tuple[Candidate, Data]?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants