Skip to content

Commit 5995713

Browse files
dhananjai1729dhananjai1729claudealexzhang13
authored
feat: parallelize rlm_query_batched subcalls for faster execution (#139)
* feat: parallelize rlm_query_batched subcalls using ThreadPoolExecutor When rlm_query_batched fans out multiple independent child RLM calls, they now run concurrently via a bounded thread pool instead of sequentially. This reduces wall time from N*T to ~T for N I/O-bound subcalls. Key changes: - Use ThreadPoolExecutor in _rlm_query_batched for 2+ prompts - Add max_concurrent_subcalls parameter (default 4) with semaphore-style concurrency control at each recursion level - Propagate setting from RLM -> LocalREPL -> child RLMs - Thread-safe completion collection with deterministic ordering - Skip thread pool for 0-1 prompts (no overhead) Closes #135 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * move max_concurrent_subcalls as a general REPL property. add more tests --------- Co-authored-by: dhananjai1729 <dhananjaikrishnakumarpm@Dhananjais-MacBook-Pro.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Alex Zhang <alex.lx.zhang@gmail.com>
1 parent 2e5764b commit 5995713

4 files changed

Lines changed: 346 additions & 9 deletions

File tree

‎rlm/core/rlm.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(
7070
custom_sub_tools: dict[str, Any] | None = None,
7171
compaction: bool = False,
7272
compaction_threshold_pct: float = 0.85,
73+
max_concurrent_subcalls: int = 4,
7374
on_subcall_start: Callable[[int, str, str], None] | None = None,
7475
on_subcall_complete: Callable[[int, str, float, str | None], None] | None = None,
7576
on_iteration_start: Callable[[int, int], None] | None = None,
@@ -102,6 +103,8 @@ def __init__(
102103
when root context reaches compaction_threshold_pct of the model's context limit.
103104
compaction_threshold_pct: When compaction is on, trigger summarization when root
104105
message token count reaches this fraction of the model context limit (default 0.85).
106+
max_concurrent_subcalls: Maximum number of parallel threads for rlm_query_batched subcalls.
107+
Each child RLM runs in its own thread. Default 4.
105108
on_subcall_start: Callback fired when a child RLM starts. Args: (depth, model, prompt_preview).
106109
on_subcall_complete: Callback fired when a child RLM completes. Args: (depth, model, duration, error_or_none).
107110
on_iteration_start: Callback fired when an iteration starts. Args: (depth, iteration_num).
@@ -132,6 +135,7 @@ def __init__(
132135

133136
self.compaction = compaction
134137
self.compaction_threshold_pct = compaction_threshold_pct
138+
self.max_concurrent_subcalls = max_concurrent_subcalls
135139

136140
self.depth = depth
137141
self.max_depth = max_depth
@@ -238,6 +242,7 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
238242
env_kwargs["custom_sub_tools"] = self.custom_sub_tools
239243
if self.compaction and self.environment_type == "local":
240244
env_kwargs["compaction"] = True
245+
env_kwargs["max_concurrent_subcalls"] = self.max_concurrent_subcalls
241246
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
242247

243248
if self.persistent:
@@ -765,6 +770,8 @@ def _subcall(self, prompt: str, model: str | None = None) -> RLMChatCompletion:
765770
# Propagate custom tools to children (sub_tools become the child's tools)
766771
custom_tools=self.custom_sub_tools,
767772
custom_sub_tools=self.custom_sub_tools,
773+
# Propagate concurrency settings to children
774+
max_concurrent_subcalls=self.max_concurrent_subcalls,
768775
# Propagate callbacks to children for nested tracking
769776
on_subcall_start=self.on_subcall_start,
770777
on_subcall_complete=self.on_subcall_complete,

‎rlm/environments/base_env.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,12 @@ class BaseEnv(ABC):
212212
}
213213
"""
214214

215-
def __init__(self, persistent: bool = False, depth: int = 1, **kwargs):
215+
def __init__(
216+
self, persistent: bool = False, depth: int = 1, max_concurrent_subcalls: int = 4, **kwargs
217+
):
216218
self.persistent = persistent
217219
self.depth = depth
220+
self.max_concurrent_subcalls = max_concurrent_subcalls
218221
self.kwargs = kwargs
219222

220223
@abstractmethod

‎rlm/environments/local_repl.py‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import time
1010
import uuid
1111
from collections.abc import Callable
12+
from concurrent.futures import ThreadPoolExecutor, as_completed
1213
from contextlib import contextmanager
1314
from typing import Any
1415

@@ -135,9 +136,15 @@ def __init__(
135136
custom_tools: dict[str, Any] | None = None,
136137
custom_sub_tools: dict[str, Any] | None = None,
137138
compaction: bool = False,
139+
max_concurrent_subcalls: int = 4,
138140
**kwargs,
139141
):
140-
super().__init__(persistent=persistent, depth=depth, **kwargs)
142+
super().__init__(
143+
persistent=persistent,
144+
depth=depth,
145+
max_concurrent_subcalls=max_concurrent_subcalls,
146+
**kwargs,
147+
)
141148

142149
self.lm_handler_address = lm_handler_address
143150
self.subcall_fn = subcall_fn # Callback for recursive RLM calls (depth > 1 support)
@@ -316,9 +323,13 @@ def _rlm_query(self, prompt: str, model: str | None = None) -> str:
316323
return self._llm_query(prompt, model)
317324

318325
def _rlm_query_batched(self, prompts: list[str], model: str | None = None) -> list[str]:
319-
"""Spawn recursive RLM sub-calls for multiple prompts.
326+
"""Spawn recursive RLM sub-calls for multiple prompts in parallel.
327+
328+
Each prompt gets its own child RLM for deeper thinking. When multiple
329+
prompts are provided, subcalls run concurrently using a thread pool
330+
(bounded by max_concurrent_subcalls) since they are independent and
331+
I/O-bound. Results are returned in the same order as input prompts.
320332
321-
Each prompt gets its own child RLM for deeper thinking.
322333
Falls back to llm_query_batched if no recursive capability is configured.
323334
324335
Args:
@@ -329,14 +340,47 @@ def _rlm_query_batched(self, prompts: list[str], model: str | None = None) -> li
329340
List of responses in the same order as input prompts.
330341
"""
331342
if self.subcall_fn is not None:
332-
results = []
333-
for prompt in prompts:
343+
# For 0 or 1 prompts, no need for thread pool overhead
344+
if len(prompts) <= 1:
345+
results = []
346+
for prompt in prompts:
347+
try:
348+
completion = self.subcall_fn(prompt, model)
349+
self._pending_llm_calls.append(completion)
350+
results.append(completion.response)
351+
except Exception as e:
352+
results.append(f"Error: RLM query failed - {e}")
353+
return results
354+
355+
# Parallel execution for multiple prompts
356+
max_workers = min(self.max_concurrent_subcalls, len(prompts))
357+
# Pre-allocate result slots to preserve ordering
358+
results: list[str] = [""] * len(prompts)
359+
completions: list[tuple[int, RLMChatCompletion]] = []
360+
lock = threading.Lock()
361+
362+
def _run_subcall(index: int, prompt: str) -> None:
334363
try:
335364
completion = self.subcall_fn(prompt, model)
336-
self._pending_llm_calls.append(completion)
337-
results.append(completion.response)
365+
with lock:
366+
completions.append((index, completion))
367+
results[index] = completion.response
338368
except Exception as e:
339-
results.append(f"Error: RLM query failed - {e}")
369+
results[index] = f"Error: RLM query failed - {e}"
370+
371+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
372+
futures = [
373+
executor.submit(_run_subcall, i, prompt) for i, prompt in enumerate(prompts)
374+
]
375+
# Wait for all futures to complete; exceptions are captured inside _run_subcall
376+
for future in as_completed(futures):
377+
future.result() # Re-raises unexpected executor errors
378+
379+
# Append completions in original prompt order for deterministic metadata
380+
completions.sort(key=lambda x: x[0])
381+
for _, completion in completions:
382+
self._pending_llm_calls.append(completion)
383+
340384
return results
341385

342386
# Fall back to plain batched LM call if no recursive capability

0 commit comments

Comments
 (0)