Skip to content

Commit 2e5764b

Browse files
authored
Change asyncio.run_all() --> semaphore queue for sub-calls (alexzhang13#131)
* cap batching to 16 * add mock LM for tests, add test for semaphore * remove mock lm in rlm/ and update into tests * linting * more linting --------- Co-authored-by: Ubuntu <az@primeintellect.ai>
1 parent 413c373 commit 2e5764b

3 files changed

Lines changed: 99 additions & 12 deletions

File tree

‎rlm/core/lm_handler.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ def _handle_batched(self, request: LMRequest, handler: "LMHandler") -> LMRespons
8585

8686
start_time = time.perf_counter()
8787

88+
sem = asyncio.Semaphore(handler.batch_max_concurrent)
89+
90+
async def run_one(prompt: str):
91+
async with sem:
92+
return await client.acompletion(prompt)
93+
8894
async def run_all():
89-
tasks = [client.acompletion(prompt) for prompt in request.prompts]
95+
tasks = [run_one(prompt) for prompt in request.prompts]
9096
return await asyncio.gather(*tasks)
9197

9298
results = asyncio.run(run_all())
@@ -132,6 +138,7 @@ def __init__(
132138
host: str = "127.0.0.1",
133139
port: int = 0, # auto-assign available port
134140
other_backend_client: BaseLM | None = None,
141+
batch_max_concurrent: int = 16,
135142
):
136143
self.default_client = client
137144
self.other_backend_client = other_backend_client
@@ -140,6 +147,7 @@ def __init__(
140147
self._server: ThreadingLMServer | None = None
141148
self._thread: Thread | None = None
142149
self._port = port
150+
self.batch_max_concurrent = batch_max_concurrent
143151

144152
self.register_client(client.model_name, client)
145153

‎tests/mock_lm.py‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
1+
"""Mock LM for testing without a real model."""
2+
3+
from collections.abc import Callable
4+
from typing import Any
5+
16
from rlm.clients.base_lm import BaseLM
27
from rlm.core.types import ModelUsageSummary, UsageSummary
38

49

510
class MockLM(BaseLM):
6-
"""Simple mock LM that echoes prompts."""
11+
"""
12+
In-memory mock LM that implements BaseLM for testing.
713
8-
def __init__(self):
9-
super().__init__(model_name="mock-model")
14+
- completion() / acompletion(): echo-style response by default, or use
15+
optional responses list / response_fn if provided.
16+
- get_usage_summary() / get_last_usage(): return usage suitable for tests.
17+
"""
1018

11-
def completion(self, prompt):
12-
return f"Mock response to: {prompt[:50]}"
19+
def __init__(
20+
self,
21+
model_name: str = "mock-model",
22+
responses: list[str] | None = None,
23+
response_fn: Callable[[str | dict[str, Any]], str] | None = None,
24+
**kwargs: Any,
25+
):
26+
super().__init__(model_name=model_name, **kwargs)
27+
self._responses = list(responses) if responses is not None else None
28+
self._response_fn = response_fn
29+
self._call_count = 0
1330

14-
async def acompletion(self, prompt):
31+
def completion(self, prompt: str | dict[str, Any]) -> str:
32+
self._call_count += 1
33+
if self._responses is not None:
34+
if not self._responses:
35+
raise IndexError("MockLM: no more responses in list")
36+
return self._responses.pop(0)
37+
if self._response_fn is not None:
38+
return self._response_fn(prompt)
39+
prompt_str = prompt if isinstance(prompt, str) else str(prompt)[:80]
40+
return f"Mock response to: {prompt_str}"
41+
42+
async def acompletion(self, prompt: str | dict[str, Any]) -> str:
1543
return self.completion(prompt)
1644

17-
def get_usage_summary(self):
45+
def get_usage_summary(self) -> UsageSummary:
1846
return UsageSummary(
1947
model_usage_summaries={
20-
"mock-model": ModelUsageSummary(
21-
total_calls=1, total_input_tokens=10, total_output_tokens=10
48+
self.model_name: ModelUsageSummary(
49+
total_calls=self._call_count,
50+
total_input_tokens=self._call_count * 10,
51+
total_output_tokens=self._call_count * 10,
2252
)
2353
}
2454
)
2555

26-
def get_last_usage(self):
27-
return self.get_usage_summary()
56+
def get_last_usage(self) -> ModelUsageSummary:
57+
return ModelUsageSummary(
58+
total_calls=1,
59+
total_input_tokens=10,
60+
total_output_tokens=10,
61+
)

‎tests/test_lm_handler.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for LMHandler using MockLM (no real LM required)."""
2+
3+
from rlm.core.comms_utils import LMRequest, send_lm_request, send_lm_request_batched
4+
from rlm.core.lm_handler import LMHandler
5+
from tests.mock_lm import MockLM
6+
7+
8+
def test_lm_handler_single_request():
9+
"""Single prompt request returns success and echo-style content."""
10+
mock = MockLM(responses=["hello back"])
11+
with LMHandler(client=mock) as handler:
12+
request = LMRequest(prompt="hello")
13+
response = send_lm_request(handler.address, request)
14+
assert response.success
15+
assert response.chat_completion is not None
16+
assert response.chat_completion.response == "hello back"
17+
18+
19+
def test_lm_handler_batched_request():
20+
"""Batched prompts return one response per prompt in order."""
21+
responses = [f"r{i}" for i in range(5)]
22+
mock = MockLM(responses=responses)
23+
with LMHandler(client=mock, batch_max_concurrent=3) as handler:
24+
prompts = [f"prompt-{i}" for i in range(5)]
25+
result = send_lm_request_batched(handler.address, prompts)
26+
assert len(result) == 5
27+
for i, resp in enumerate(result):
28+
assert resp.success, resp.error
29+
assert resp.chat_completion is not None
30+
assert resp.chat_completion.response == f"r{i}"
31+
32+
33+
def test_lm_handler_batched_many_prompts_semaphore_cap():
34+
"""Many prompts complete successfully with semaphore limiting concurrency."""
35+
# 50 prompts, max 4 concurrent: should still all complete
36+
count = 50
37+
responses = [f"resp-{i}" for i in range(count)]
38+
mock = MockLM(responses=responses)
39+
with LMHandler(client=mock, batch_max_concurrent=4) as handler:
40+
prompts = [f"p-{i}" for i in range(count)]
41+
result = send_lm_request_batched(handler.address, prompts)
42+
assert len(result) == count
43+
for i, resp in enumerate(result):
44+
assert resp.success, (i, resp.error)
45+
assert resp.chat_completion.response == f"resp-{i}"

0 commit comments

Comments
 (0)