Skip to content

Commit 460595c

Browse files
authored
Merge branch 'main' into parallel-rlm-subcalls
2 parents bc52f44 + 4eba6ec commit 460595c

5 files changed

Lines changed: 333 additions & 26 deletions

File tree

‎examples/quickstart.py‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
import random
3+
import string
24

35
from dotenv import load_dotenv
46

@@ -7,21 +9,31 @@
79

810
load_dotenv()
911

10-
logger = RLMLogger(log_dir="./logs")
12+
# Generate a large text file with a hidden secret number
13+
secret_number = random.randint(100_000_000, 999_999_999)
14+
filler_lines = ["".join(random.choices(string.ascii_lowercase + " ", k=120)) for _ in range(50_000)]
15+
insert_at = random.randint(len(filler_lines) // 3, 2 * len(filler_lines) // 3)
16+
filler_lines.insert(insert_at, f"SECRET_NUMBER={secret_number}")
17+
haystack = "\n".join(filler_lines)
1118

1219
rlm = RLM(
13-
backend="portkey", # or "openai", etc.
20+
backend="openai",
1421
backend_kwargs={
15-
"model_name": "@openai/gpt-5-nano",
16-
"api_key": os.getenv("PORTKEY_API_KEY"),
22+
"model_name": "gpt-5-nano",
23+
"api_key": os.getenv("OPENAI_API_KEY"),
1724
},
1825
environment="local",
19-
environment_kwargs={},
20-
max_depth=1,
21-
logger=logger,
22-
verbose=True, # For printing to console with rich, disabled by default.
26+
max_iterations=10,
27+
logger=RLMLogger(log_dir="./logs"),
28+
verbose=True,
2329
)
2430

25-
result = rlm.completion("Print me the first 5 powers of two, each on a newline.")
31+
result = rlm.completion(
32+
"The context contains ~50k lines of random text with a single line "
33+
"matching the pattern SECRET_NUMBER=<digits>. Find and return ONLY the "
34+
f"numeric value.\n\n{haystack}"
35+
)
2636

27-
print(result)
37+
print(f"\nModel found: {result.response}")
38+
print(f"Actual number: {secret_number}")
39+
print(f"Correct: {str(secret_number) in result.response}")

‎rlm/core/rlm.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
248248
env_kwargs["custom_sub_tools"] = self.custom_sub_tools
249249
if self.compaction and self.environment_type == "local":
250250
env_kwargs["compaction"] = True
251+
env_kwargs["max_concurrent_subcalls"] = self.max_concurrent_subcalls
251252
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
252253

253254
if self.persistent:

‎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: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,12 @@ def __init__(
172172
subcall_semaphore: threading.Semaphore | None = None,
173173
**kwargs,
174174
):
175-
"""
176-
Args:
177-
max_concurrent_subcalls: Max children to dispatch in parallel from
178-
THIS REPL instance (controls local ThreadPoolExecutor size).
179-
Default 1 = sequential (identical to previous behavior).
180-
subcall_semaphore: Global semaphore bounding total concurrent child
181-
RLMs across ALL depths. If None, uses the module-level default
182-
(16 slots). Pass a custom one for testing or tighter control.
183-
"""
184-
super().__init__(persistent=persistent, depth=depth, **kwargs)
175+
super().__init__(
176+
persistent=persistent,
177+
depth=depth,
178+
max_concurrent_subcalls=max_concurrent_subcalls,
179+
**kwargs,
180+
)
185181

186182
self.lm_handler_address = lm_handler_address
187183
self.subcall_fn = subcall_fn # Callback for recursive RLM calls (depth > 1 support)
@@ -363,8 +359,7 @@ def _rlm_query(self, prompt: str, model: str | None = None) -> str:
363359
return self._llm_query(prompt, model)
364360

365361
def _rlm_query_batched(self, prompts: list[str], model: str | None = None) -> list[str]:
366-
"""Spawn recursive RLM sub-calls for multiple prompts.
367-
362+
"""Spawn recursive RLM sub-calls for multiple prompts in parallel.
368363
Each prompt gets its own child RLM for deeper thinking.
369364
370365
Execution modes (controlled by max_concurrent_subcalls):
@@ -401,10 +396,25 @@ def _rlm_query_batched(self, prompts: list[str], model: str | None = None) -> li
401396
for prompt in prompts:
402397
try:
403398
completion = self.subcall_fn(prompt, model)
404-
self._pending_llm_calls.append(completion)
405-
results.append(completion.response)
399+
with lock:
400+
completions.append((index, completion))
401+
results[index] = completion.response
406402
except Exception as e:
407-
results.append(f"Error: RLM query failed - {e}")
403+
results[index] = f"Error: RLM query failed - {e}"
404+
405+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
406+
futures = [
407+
executor.submit(_run_subcall, i, prompt) for i, prompt in enumerate(prompts)
408+
]
409+
# Wait for all futures to complete; exceptions are captured inside _run_subcall
410+
for future in as_completed(futures):
411+
future.result() # Re-raises unexpected executor errors
412+
413+
# Append completions in original prompt order for deterministic metadata
414+
completions.sort(key=lambda x: x[0])
415+
for _, completion in completions:
416+
self._pending_llm_calls.append(completion)
417+
408418
return results
409419

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

0 commit comments

Comments
 (0)