From b9c2ae40e0ef6c024fcedbecb2f48ab15b1850bb Mon Sep 17 00:00:00 2001 From: Kevin Guan Date: Thu, 8 Jan 2026 15:42:29 +0000 Subject: [PATCH] initial optimizer feature Signed-off-by: Kevin Guan --- .../src/openroad_mcp/server/orfs/orfs_base.py | 11 ++-- .../src/openroad_mcp/server/orfs/orfs_make.py | 50 ++++++++------- .../server/orfs/orfs_optimizer.py | 64 +++++++++++++++++++ .../openroad_mcp/server/orfs/orfs_server.py | 6 +- backend/src/prompts/tool_examples.py | 10 +++ 5 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 backend/src/openroad_mcp/server/orfs/orfs_optimizer.py diff --git a/backend/src/openroad_mcp/server/orfs/orfs_base.py b/backend/src/openroad_mcp/server/orfs/orfs_base.py index 3dd2f83c..a916c8f8 100644 --- a/backend/src/openroad_mcp/server/orfs/orfs_base.py +++ b/backend/src/openroad_mcp/server/orfs/orfs_base.py @@ -18,6 +18,11 @@ def _get_platforms_impl(self) -> str: ORFS.server.platform = "sky130hd" return ORFS.server.platform + def _make(self, cmd) -> None: + assert ORFS.server is not None + ORFS.server._check_configuration() + ORFS.server._command(cmd) + def _get_designs_impl(self) -> str: """Internal implementation of get_designs""" # TODO: scrape designs instead of default riscv @@ -40,7 +45,7 @@ def _command(self, cmd: str) -> None: working = os.getcwd() os.chdir(ORFS.server.flow_dir) - make = f"make DESIGN_CONFIG={ORFS.server.flow_dir}/designs/{ORFS.server.platform}/{ORFS.server.design}/config.mk" + make = f"make DESIGN_CONFIG={ORFS.server.makefile_pointer}" logging.info(cmd) build_command = f"{make} {cmd}" ORFS.server._run_command(build_command) @@ -99,9 +104,7 @@ def make(cmd: str) -> str: Use this for any makefile target not covered by step/jump commands. """ - assert ORFS.server is not None - ORFS.server._check_configuration() - ORFS.server._command(cmd) + ORFS.server._make(cmd) return f"finished {cmd}" diff --git a/backend/src/openroad_mcp/server/orfs/orfs_make.py b/backend/src/openroad_mcp/server/orfs/orfs_make.py index 75964125..e5c5eb80 100644 --- a/backend/src/openroad_mcp/server/orfs/orfs_make.py +++ b/backend/src/openroad_mcp/server/orfs/orfs_make.py @@ -19,6 +19,31 @@ def _get_default_makefile(self) -> None: assert ORFS.server is not None ORFS.server.makefile_pointer = f"{ORFS.server.flow_dir}/designs/{ORFS.server.platform}/{ORFS.server.design}/config.mk" + def _create_dynamic_makefile(self) -> None: + """Create dynamic makefile.""" + assert ORFS.server is not None + if not (ORFS.server.design and ORFS.server.platform): + logging.warning("no custom design/platform selected!") + ORFS.server._get_designs_impl() + ORFS.server._get_platforms_impl() + ORFS.server._get_default_env() + else: + pass + + ORFS.server.dynamic_makefile = True + ORFS.server.makefile_pointer = f"{ORFS.server.flow_dir}/designs/{ORFS.server.platform}/{ORFS.server.design}/dynamic_config.mk" + with open(f"{ORFS.server.makefile_pointer}", "w") as f: + for key in ORFS.server.orfs_env.keys(): + f.write(f"export {key} = {ORFS.server.orfs_env[key]}\n") + result = "" + for key in ORFS.server.orfs_env.keys(): + result += f"{key}: {ORFS.server.orfs_env[key]}\n" + if result: + return result + else: + return "no env vars" + + def _get_makefile(self) -> None: """Retrieve the current makefile pointer path.""" assert ORFS.server is not None @@ -32,7 +57,8 @@ def _get_default_env(self) -> None: ORFS.server.orfs_env.update( { "PLATFORM": f"{ORFS.server.platform}", - "DESIGN_NAME": f"{ORFS.server.design}", + #"DESIGN_NAME": f"{ORFS.server.design}", + "DESIGN_NAME": "riscv", # TODO: temporary fix for default design "DESIGN_NICKNAME": f"{ORFS.server.design}", "VERILOG_FILES": "$(sort $(wildcard ./designs/src/$(DESIGN_NICKNAME)/*.v))", "SDC_FILE": "./designs/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint.sdc", @@ -77,27 +103,7 @@ def create_dynamic_makefile(cmd: str) -> str: Note: File is written to: {flow_dir}/designs/{platform}/{design}/dynamic_config.mk """ - assert ORFS.server is not None - if not (ORFS.server.design and ORFS.server.platform): - logging.warning("no custom design/platform selected!") - ORFS.server._get_designs_impl() - ORFS.server._get_platforms_impl() - ORFS.server._get_default_env() - else: - pass - - ORFS.server.dynamic_makefile = True - ORFS.server.makefile_pointer = f"{ORFS.server.flow_dir}/designs/{ORFS.server.platform}/{ORFS.server.design}/dynamic_config.mk" - with open(f"{ORFS.server.makefile_pointer}", "w") as f: - for key in ORFS.server.orfs_env.keys(): - f.write(f"export {key} = {ORFS.server.orfs_env[key]}\n") - result = "" - for key in ORFS.server.orfs_env.keys(): - result += f"{key}: {ORFS.server.orfs_env[key]}\n" - if result: - return result - else: - return "no env vars" + ORFS.server._create_dynamic_makefile() @staticmethod @ORFS.mcp.tool diff --git a/backend/src/openroad_mcp/server/orfs/orfs_optimizer.py b/backend/src/openroad_mcp/server/orfs/orfs_optimizer.py new file mode 100644 index 00000000..bec0676c --- /dev/null +++ b/backend/src/openroad_mcp/server/orfs/orfs_optimizer.py @@ -0,0 +1,64 @@ +import json +import scipy + +from src.openroad_mcp.server.orfs.orfs_tools import ORFS + +class ORFSOptimizer(ORFS): + @staticmethod + @ORFS.mcp.tool + def calc_cost() -> str: + """ + Calculate cost function + """ + result_list = [] + + param_keys = ["CORE_UTILIZATION"] + BOUNDS = [(0.01, 0.99)] + PARAMS = [0.05] + + def evaluate(target): + # Force step increments of at least 0.01 + x = [max(BOUNDS[i][0], min(BOUNDS[i][1], round(target[i]/0.01)*0.01)) for i in range(len(target))] + + modify = {param_keys[0]: x[0]*100} + ORFS.server.orfs_env.update(modify) + + # Run flow + ORFS.server._create_dynamic_makefile() + ORFS.server._make("clean_floorplan") + ORFS.server._make("floorplan") + ORFS.server._make("update_metadata") + + # Read metric + with open(f"{ORFS.server.flow_dir}/designs/{ORFS.server.platform}/{ORFS.server.design}/metadata-base-ok.json", 'r') as f: + metrics = json.load(f) + + # Floorplan die area as cost + cost = metrics["floorplan__design__die__area"] + result_list.append(x[0]*100) + return cost + + def report(xk): + print("Current params:", xk) + + ORFS.server.opt_method = "Powell" + ORFS.server.logging(f"Running {ORFS.server.opt_method} optimizer...") + + result = scipy.optimize.minimize( + evaluate, + PARAMS, + bounds=BOUNDS, + method=ORFS.server.opt_method, + options={ + "maxiter": 1000, + "xtol": 0.01, # minimum change in x before stopping + "ftol": 0.0, # don't stop due to small cost change + "disp": True, + }, + callback=report + ) + + ORFS.server.logging(result) + ORFS.server.logging(result_list) + + return "" diff --git a/backend/src/openroad_mcp/server/orfs/orfs_server.py b/backend/src/openroad_mcp/server/orfs/orfs_server.py index e7f226eb..00a9f323 100644 --- a/backend/src/openroad_mcp/server/orfs/orfs_server.py +++ b/backend/src/openroad_mcp/server/orfs/orfs_server.py @@ -8,6 +8,7 @@ from src.openroad_mcp.server.orfs.orfs_make import ORFSMake from src.openroad_mcp.server.orfs.orfs_base import ORFSBase from src.openroad_mcp.server.orfs.orfs_rag import ORFSRag +from src.openroad_mcp.server.orfs.orfs_optimizer import ORFSOptimizer logging.basicConfig( level=os.environ.get("LOGLEVEL", "INFO").upper(), @@ -25,7 +26,7 @@ class ORFSEnv(TypedDict): routing: list[str | None] -class ORFSServer(ORFSBase, ORFSMake, ORFSRag): +class ORFSServer(ORFSBase, ORFSMake, ORFSRag, ORFSOptimizer): def __init__(self) -> None: ORFS.server = self @@ -60,6 +61,9 @@ def _setup_env(self) -> None: raise ValueError("ORFS_DIR environment variable is not set") self.flow_dir = os.path.join(self.orfs_dir, "flow") + def logging(self, msg): + logging.info(msg) + if __name__ == "__main__": server = ORFSServer() diff --git a/backend/src/prompts/tool_examples.py b/backend/src/prompts/tool_examples.py index 65573f48..48b1ce0e 100644 --- a/backend/src/prompts/tool_examples.py +++ b/backend/src/prompts/tool_examples.py @@ -73,6 +73,16 @@ "content": """{{ "name": "step", "args": {{}} +}}""", + }, + ### + ### optimize + {"role": "user", "content": "Calculate cost function"}, + { + "role": "assistant", + "content": """{{ + "name": "calc_cost", + "args": {{}} }}""", }, ###