feat: async/parallel execution with configurable concurrency
Parallelize LLM calls across minibatches to reduce wall-clock time. All domain ports (LLMPort, JudgePort, ProposerPort) are now async. Adapter implementations wrap synchronous DSPy calls with asyncio.to_thread. Judge calls run in parallel within a batch using asyncio.gather + semaphore. Evaluator parallelizes minibatch execution with configurable concurrency. Evolution loop and use case are fully async. Proposer stays sequential. Added --max-concurrency CLI flag and max_concurrency YAML config field. Added async_retry_with_backoff for async error handling. All 139 unit tests pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -57,23 +57,25 @@ def synth_lm() -> dspy.LM:
|
||||
class TestDSPyLLMAdapterOwnLM:
|
||||
"""Bug #2 fix: DSPyLLMAdapter must use the LM it receives, not the global one."""
|
||||
|
||||
def test_uses_provided_lm_not_global(self) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_provided_lm_not_global(self) -> None:
|
||||
local_lm = dspy.utils.DummyLM([{"output": "local response"}])
|
||||
global_lm = dspy.utils.DummyLM([{"output": "global response"}])
|
||||
dspy.configure(lm=global_lm)
|
||||
|
||||
adapter = DSPyLLMAdapter(lm=local_lm)
|
||||
result = adapter.execute(Prompt(text="test"), "input")
|
||||
result = await adapter.execute(Prompt(text="test"), "input")
|
||||
|
||||
assert result == "local response"
|
||||
|
||||
def test_does_not_affect_global_lm(self) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_affect_global_lm(self) -> None:
|
||||
local_lm = dspy.utils.DummyLM([{"output": "local response"}])
|
||||
global_lm = dspy.utils.DummyLM([{"output": "global response"}])
|
||||
dspy.configure(lm=global_lm)
|
||||
|
||||
adapter = DSPyLLMAdapter(lm=local_lm)
|
||||
adapter.execute(Prompt(text="test"), "input")
|
||||
await adapter.execute(Prompt(text="test"), "input")
|
||||
|
||||
# Global LM should still be the same
|
||||
assert dspy.settings.lm is global_lm
|
||||
@@ -82,9 +84,10 @@ class TestDSPyLLMAdapterOwnLM:
|
||||
class TestDSPyJudgeAdapterOwnLM:
|
||||
"""DSPyJudgeAdapter must use its own LM instance."""
|
||||
|
||||
def test_uses_provided_lm(self, judge_lm: dspy.LM) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_provided_lm(self, judge_lm: dspy.LM) -> None:
|
||||
adapter = DSPyJudgeAdapter(lm=judge_lm)
|
||||
results = adapter.judge_batch(
|
||||
results = await adapter.judge_batch(
|
||||
task_description="Test task",
|
||||
pairs=[("input 1", "output 1")],
|
||||
)
|
||||
@@ -93,7 +96,8 @@ class TestDSPyJudgeAdapterOwnLM:
|
||||
assert score == 0.8
|
||||
assert feedback == "Good response."
|
||||
|
||||
def test_does_not_use_global_lm(self) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_use_global_lm(self) -> None:
|
||||
judge_lm = dspy.utils.DummyLM(
|
||||
[{"reasoning": "ok", "score": "0.9", "feedback": "Judge-specific response"}]
|
||||
)
|
||||
@@ -101,14 +105,15 @@ class TestDSPyJudgeAdapterOwnLM:
|
||||
dspy.configure(lm=global_lm)
|
||||
|
||||
adapter = DSPyJudgeAdapter(lm=judge_lm)
|
||||
results = adapter.judge_batch("task", [("in", "out")])
|
||||
results = await adapter.judge_batch("task", [("in", "out")])
|
||||
assert results[0][0] == 0.9
|
||||
|
||||
|
||||
class TestDSPyProposerAdapterOwnLM:
|
||||
"""DSPyProposerAdapter must use its own LM instance."""
|
||||
|
||||
def test_uses_provided_lm(self, proposer_lm: dspy.LM) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_provided_lm(self, proposer_lm: dspy.LM) -> None:
|
||||
adapter = DSPyProposerAdapter(lm=proposer_lm)
|
||||
trajectories = [
|
||||
Trajectory(
|
||||
@@ -119,14 +124,15 @@ class TestDSPyProposerAdapterOwnLM:
|
||||
prompt_used="old prompt",
|
||||
)
|
||||
]
|
||||
result = adapter.propose(
|
||||
result = await adapter.propose(
|
||||
current_prompt=Prompt(text="old prompt"),
|
||||
trajectories=trajectories,
|
||||
task_description="Test task",
|
||||
)
|
||||
assert "Improved prompt" in result.text
|
||||
|
||||
def test_does_not_use_global_lm(self) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_use_global_lm(self) -> None:
|
||||
proposer_lm = dspy.utils.DummyLM(
|
||||
[{"reasoning": "ok", "new_instruction": "proposer-specific"}]
|
||||
)
|
||||
@@ -136,7 +142,7 @@ class TestDSPyProposerAdapterOwnLM:
|
||||
dspy.configure(lm=global_lm)
|
||||
|
||||
adapter = DSPyProposerAdapter(lm=proposer_lm)
|
||||
result = adapter.propose(
|
||||
result = await adapter.propose(
|
||||
current_prompt=Prompt(text="test"),
|
||||
trajectories=[],
|
||||
task_description="task",
|
||||
|
||||
Reference in New Issue
Block a user