Skip to content

Commit 15bf797

Browse files
feat: implement the base orchestrator abstract class
1 parent 1091cde commit 15bf797

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import abc
2+
import asyncio
3+
import logging
4+
import os
5+
import time
6+
from typing import Any, List, Optional, Tuple
7+
8+
from auto_search.graph import EvaluationResult, Node, SearchGraph
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class SearchOrchestrator(abc.ABC):
14+
def __init__(
15+
self,
16+
problem_id: str,
17+
reference_code: str,
18+
graph_db_path: Optional[str] = None,
19+
max_concurrency: int = 2,
20+
):
21+
# Resolve graph db path and run directory
22+
if not graph_db_path:
23+
workdir = os.environ.get("WORKDIR", os.getcwd())
24+
timestamp = time.strftime("%Y%m%d_%H%M%S")
25+
graph_db_path = os.path.join(
26+
workdir, f"graph_{problem_id}_{timestamp}.json"
27+
)
28+
logger.info(f"No graph_db_path provided. Generated: {graph_db_path}")
29+
30+
# The run directory is the root directory for this search run.
31+
self.run_dir = os.path.dirname(os.path.abspath(graph_db_path))
32+
logger.info(f"Using run directory: {self.run_dir}")
33+
os.makedirs(self.run_dir, exist_ok=True)
34+
35+
# Initialization of internal states
36+
self._semaphore = asyncio.Semaphore(max_concurrency)
37+
self._node_counter = 0
38+
39+
self.reference_code = reference_code
40+
41+
# Initialization of search graph
42+
self.graph = SearchGraph(problem_id, graph_db_path)
43+
if self.graph.root_id:
44+
logger.info(
45+
"Found existing graph. Call `.resume()` explicitly to restore"
46+
" search state."
47+
)
48+
else:
49+
# Start from scratch, create root node
50+
node_id, session_dir = self.get_next_session_node()
51+
os.makedirs(session_dir, exist_ok=True)
52+
# Write reference code to the root node directory
53+
with open(os.path.join(session_dir, "base_kernel.py"), "w") as f:
54+
f.write(reference_code)
55+
56+
root_node = Node(
57+
node_id=node_id,
58+
parent_id=None,
59+
session_dir=session_dir,
60+
code=reference_code,
61+
plan="",
62+
depth=0,
63+
strategy_applied="baseline",
64+
execution_status="SUCCESS",
65+
evaluation=EvaluationResult(
66+
compiled=True,
67+
correct=True,
68+
latency_ms=float("inf"),
69+
profiling_summary="",
70+
),
71+
)
72+
73+
self.graph.add_node(root_node)
74+
75+
def get_next_session_node(
76+
self, suffix: Optional[str] = None
77+
) -> Tuple[str, str]:
78+
"""Returns (node_id, session_dir) for the next node in the graph."""
79+
node_id = f"node_{self._node_counter:03d}"
80+
self._node_counter += 1
81+
dir_name = f"{node_id}_{suffix}" if suffix else node_id
82+
session_dir = os.path.join(self.run_dir, "nodes", dir_name)
83+
return node_id, session_dir
84+
85+
def resume(self) -> None:
86+
"""Restores the orchestrator state from the loaded graph's metadata."""
87+
logger.info("Resuming search orchestration from persisted graph state...")
88+
89+
# Calculate the node counter based on existing nodes in the graph
90+
existing_indices = [
91+
int(node_id.split("_")[1])
92+
for node_id in self.graph.nodes.keys()
93+
if node_id.startswith("node_") and node_id[5:].isdigit()
94+
]
95+
self._node_counter = max(existing_indices) + 1 if existing_indices else 0
96+
97+
self._resume()
98+
99+
def update_metadata(self, key: str, value: Any) -> None:
100+
"""Writes orchestrator metadata to the graph and triggers a save."""
101+
self.graph.metadata[key] = value
102+
self.graph.save()
103+
104+
# --- Template Methods ---
105+
106+
@abc.abstractmethod
107+
def _resume(self) -> None:
108+
"""Restores subclass-specific instance variables from metadata.
109+
110+
Reads from self.graph.metadata to restore internal algorithm state.
111+
"""
112+
pass
113+
114+
@abc.abstractmethod
115+
def _select_nodes_to_expand(self) -> List[Node]:
116+
"""Selects candidate nodes from the search graph for expansion.
117+
118+
Returns a list of nodes to be expanded in the current step.
119+
"""
120+
pass
121+
122+
@abc.abstractmethod
123+
def _generate_expansion_tasks(self, nodes: List[Node]) -> Any:
124+
"""Generates expansion tasks for the selected nodes.
125+
126+
Creates tasks to be passed directly into _execute_expansions.
127+
"""
128+
pass
129+
130+
@abc.abstractmethod
131+
def _update_search_state(self, new_nodes: List[Node]) -> None:
132+
"""Updates internal algorithm state using evaluated nodes.
133+
134+
Updates state such as beam frontier or learnings based on newly evaluated
135+
nodes from worker expansions.
136+
"""
137+
pass
138+
139+
@abc.abstractmethod
140+
def _should_terminate(self) -> bool:
141+
"""Determines whether the search loop should terminate.
142+
143+
Returns True if termination criteria (e.g., max depth reached or no
144+
candidates) are met.
145+
"""
146+
pass
147+
148+
def _post_step_hook(self) -> None:
149+
"""Optional hook executed at the end of each search step.
150+
151+
Can be overridden for cleanup, state updates, or logging.
152+
"""
153+
pass
154+
155+
# --- Concrete Flow ---
156+
157+
@abc.abstractmethod
158+
async def _execute_expansions(self, tasks: Any) -> List[Node]:
159+
"""Executes expansion tasks to evaluate new kernels.
160+
161+
Runs tasks (typically in parallel via workers) to compile, test, and profile
162+
candidate kernels, returning new Nodes.
163+
"""
164+
pass
165+
166+
async def run(self) -> Node:
167+
logger.info(
168+
f"Starting search orchestration for problem: {self.graph.problem_id}"
169+
)
170+
171+
while not self._should_terminate():
172+
# 1. Select nodes to expand
173+
nodes_to_expand = self._select_nodes_to_expand()
174+
if not nodes_to_expand:
175+
break
176+
177+
# 2. Generate strategies
178+
tasks = self._generate_expansion_tasks(nodes_to_expand)
179+
if not tasks:
180+
break
181+
182+
# 3. Parallel Execution (Workers compile, test, and profile)
183+
new_nodes = await self._execute_expansions(tasks)
184+
185+
# 4. Add any nodes not already saved during execution
186+
for node in new_nodes:
187+
if node.node_id not in self.graph.nodes:
188+
self.graph.add_node(node)
189+
190+
# 5. Update Search Algorithm State
191+
self._update_search_state(new_nodes)
192+
193+
# Log progress
194+
best_id = self.graph.best_node_id
195+
best_latency = "N/A"
196+
if best_id:
197+
best_node = self.graph.get_node(best_id)
198+
if best_node and best_node.evaluation.latency_ms is not None:
199+
best_latency = f"{best_node.evaluation.latency_ms:.3f} ms"
200+
logger.info(
201+
f"Step completed. Total nodes in graph: {len(self.graph.nodes)}. "
202+
f"Current best latency: {best_latency}"
203+
)
204+
205+
# 6. Step Cleanup
206+
self._post_step_hook()
207+
208+
best_id = self.graph.best_node_id
209+
return (
210+
self.graph.get_node(best_id)
211+
if best_id
212+
else self.graph.get_node(self.graph.root_id)
213+
)

0 commit comments

Comments
 (0)