diff --git a/.gitignore b/.gitignore index cae6283..2c02b59 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,7 @@ triton_kernel_logs/ session_*/ worker_*/ .fuse/ +outputs/ # Generated kernels kernel.py diff --git a/Fuser/auto_agent.py b/Fuser/auto_agent.py index 4c2a141..fdbebb3 100644 --- a/Fuser/auto_agent.py +++ b/Fuser/auto_agent.py @@ -33,26 +33,41 @@ If the chosen path fails, the agent can optionally fall back to the other path. -CLI: - python -m Fuser.auto_agent --problem /abs/path/to/problem.py \ - [--ka-model gpt-5] [--extract-model gpt-5] [--dispatch-model o4-mini] [--compose-model o4-mini] \ - [--verify] [--no-fallback] - +CLI (Hydra-based): + python -m Fuser.auto_agent problem=/abs/path/to/problem.py + + # Override config values: + python -m Fuser.auto_agent problem=/abs/path/to/problem.py \ + ka.model=gpt-5 \ + router.model=gpt-5 \ + fuser.extracter.model=gpt-5 \ + fuser.dispatcher.model=o4-mini \ + fuser.composer.model=o4-mini \ + fuser.composer.verify=true \ + routing.allow_fallback=false + + # Or use a custom config: + python -m Fuser.auto_agent --config-name custom_auto_agent problem=/abs/path/to/problem.py + +Config file: configs/pipeline/auto_agent.yaml Returns a JSON summary to stdout and writes the generated kernel path (if available). """ from __future__ import annotations -import argparse import ast import hashlib import json import sys + +from hydra import main as hydra_main + from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Optional, Tuple from dotenv import load_dotenv +from omegaconf import DictConfig from Fuser.pipeline import run_pipeline # Local imports (available inside repo) @@ -668,64 +683,40 @@ def _llm_decide_route( # ------------------------ -def main(argv: Optional[list[str]] = None) -> int: - p = argparse.ArgumentParser( - description="Auto-router for KernelBench problems (KernelAgent vs Fuser)" - ) - p.add_argument("--problem", required=True, help="Absolute path to the problem file") - p.add_argument( - "--ka-model", - default=None, - help="Model for KernelAgent (optional; uses env default if omitted)", - ) - p.add_argument("--ka-workers", type=int, default=4) - p.add_argument("--ka-rounds", type=int, default=10) - p.add_argument("--no-ka-high-reasoning", action="store_true") - p.add_argument("--router-model", default="gpt-5") - p.add_argument("--no-router-high-reasoning", action="store_true") - p.add_argument("--router-temp", type=float, default=0.2) - p.add_argument("--router-max-tokens", type=int, default=700) - p.add_argument("--extract-model", default="gpt-5") - p.add_argument("--dispatch-model", default="o4-mini") - p.add_argument("--compose-model", default="o4-mini") - p.add_argument("--workers", type=int, default=4) - p.add_argument("--max-iters", type=int, default=5) - p.add_argument("--llm-timeout-s", type=int, default=1200) - p.add_argument("--run-timeout-s", type=int, default=1200) - p.add_argument("--compose-max-iters", type=int, default=5) - p.add_argument("--verify", action="store_true") - p.add_argument("--dispatch-jobs", type=int, default=2) - p.add_argument("--no-fallback", action="store_true") - args = p.parse_args(argv) - +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="auto_agent", +) +def main(cfg: DictConfig) -> int: # Load environment variables from .env file load_dotenv() - problem_path = Path(args.problem).resolve() + problem_path = Path(cfg.problem).resolve() if not problem_path.is_file(): print(f"problem not found: {problem_path}", file=sys.stderr) return 2 router = AutoKernelRouter( - ka_model=args.ka_model, - ka_num_workers=args.ka_workers, - ka_max_rounds=args.ka_rounds, - ka_high_reasoning=(not args.no_ka_high_reasoning), - router_model=args.router_model, - router_high_reasoning=(not args.no_router_high_reasoning), - router_temperature=args.router_temp, - router_max_tokens=args.router_max_tokens, - extract_model=args.extract_model, - dispatch_model=args.dispatch_model, - compose_model=args.compose_model, - workers=args.workers, - max_iters=args.max_iters, - llm_timeout_s=args.llm_timeout_s, - run_timeout_s=args.run_timeout_s, - compose_max_iters=args.compose_max_iters, - verify=args.verify, - dispatch_jobs=args.dispatch_jobs, - allow_fallback=(not args.no_fallback), + ka_model=cfg.ka.model_name, + ka_num_workers=cfg.ka.num_workers, + ka_max_rounds=cfg.ka.max_rounds, + ka_high_reasoning=cfg.ka.high_reasoning, + router_model=cfg.router.model, + router_high_reasoning=cfg.router.high_reasoning, + router_temperature=cfg.router.temperature, + router_max_tokens=cfg.router.max_tokens, + extract_model=cfg.fuser.extractor.model, + dispatch_model=cfg.fuser.dispatcher.model, + compose_model=cfg.fuser.composer.model, + workers=cfg.fuser.extractor.workers, + max_iters=cfg.fuser.extractor.max_iters, + llm_timeout_s=cfg.fuser.extractor.llm_timeout_s, + run_timeout_s=cfg.fuser.extractor.run_timeout_s, + compose_max_iters=cfg.fuser.composer.max_iters, + verify=cfg.fuser.composer.verify, + dispatch_jobs=cfg.fuser.dispatcher.jobs, + allow_fallback=cfg.routing.allow_fallback, ) try: diff --git a/Fuser/cli.py b/Fuser/cli.py index e8fe835..4ba8b67 100644 --- a/Fuser/cli.py +++ b/Fuser/cli.py @@ -1,5 +1,4 @@ -from __future__ import annotations - +#!/usr/bin/env python3 # Copyright (c) Meta Platforms, Inc. and affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +12,50 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import argparse +""" +Orchestrates parallel LLM workers to generate and verify +fused code. + +Runs multiple workers concurrently against a KernelBench problem file, each +worker attempting to generate a valid solution. The first worker to produce a +passing candidate wins. + +CLI (Hydra-based): + python -m Fuser.cli problem=/abs/path/to/problem.py + + # Override config values: + python -m Fuser.cli problem=/abs/path/to/problem.py \ + model=gpt-5 \ + workers=4 \ + max_iters=10 \ + stream=winner + + # Or use a custom config: + python -m Fuser.cli --config-name custom_fuser \ + problem=/abs/path/to/problem.py + +Config file: configs/pipeline/orchestrator.yaml + +Requirements: +- OPENAI_API_KEY (.env in CWD or environment) + +Outputs: +- Run directory path printed to stdout +- Artifacts in .fuse// +""" + +from __future__ import annotations import json import sys import os import multiprocessing as mp from pathlib import Path +from hydra import main as hydra_main +from omegaconf import DictConfig + +from .config import new_run_id, OrchestratorConfig from .constants import ExitCode -from .config import OrchestratorConfig, new_run_id from .paths import ensure_abs_regular_file, make_run_dirs, PathSafetyError from .logging_utils import setup_file_logger from .orchestrator import Orchestrator @@ -53,48 +87,32 @@ def _load_dotenv_if_present() -> None: pass -def cmd_run(argv: list[str]) -> int: +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="orchestrator", +) +def main(cfg: DictConfig) -> int: _load_dotenv_if_present() - p = argparse.ArgumentParser( - prog="fuse run", description="Fuse Orchestrator — first-wins runner" - ) - p.add_argument( - "--problem", required=True, help="Absolute path to the Python problem file" - ) - p.add_argument( - "--model", - default="gpt-5", - help="OpenAI model name (Responses API, default: gpt-5)", - ) - p.add_argument("--workers", type=int, default=4) - p.add_argument("--max-iters", type=int, default=10) - p.add_argument("--llm-timeout-s", type=int, default=120) - p.add_argument("--run-timeout-s", type=int, default=180) - p.add_argument("--stream", choices=["all", "winner", "none"], default="all") - p.add_argument("--store-responses", action="store_true", default=False) - p.add_argument("--isolated", action="store_true", default=False) - p.add_argument("--deny-network", action="store_true", default=False) - p.add_argument("--enable-reasoning-extras", action="store_true", default=True) - args = p.parse_args(argv) try: - problem_path = ensure_abs_regular_file(args.problem) + problem_path = ensure_abs_regular_file(cfg.problem) except PathSafetyError as e: print(str(e), file=sys.stderr) return int(ExitCode.INVALID_ARGS) - cfg = OrchestratorConfig( + orch_cfg = OrchestratorConfig( problem_path=problem_path, - model=args.model, - workers=args.workers, - max_iters=args.max_iters, - llm_timeout_s=args.llm_timeout_s, - run_timeout_s=args.run_timeout_s, - stream_mode=args.stream, - store_responses=args.store_responses, - isolated=args.isolated, - deny_network=args.deny_network, - enable_reasoning_extras=args.enable_reasoning_extras, + model=cfg.model, + workers=cfg.workers, + max_iters=cfg.max_iters, + llm_timeout_s=cfg.llm_timeout_s, + run_timeout_s=cfg.run_timeout_s, + stream_mode=cfg.stream, + store_responses=cfg.store_responses, + isolated=cfg.isolated, + deny_network=cfg.deny_network, + enable_reasoning_extras=cfg.enable_reasoning_extras, ) run_id = new_run_id() @@ -102,7 +120,10 @@ def cmd_run(argv: list[str]) -> int: try: d = make_run_dirs(FUSE_BASE_DIR, run_id) except FileExistsError: - print("Run directory already exists unexpectedly; retry.", file=sys.stderr) + print( + "Run directory already exists unexpectedly; retry.", + file=sys.stderr, + ) return int(ExitCode.GENERIC_FAILURE) orch_dir = d["orchestrator"] @@ -113,7 +134,7 @@ def cmd_run(argv: list[str]) -> int: json.dumps( { "run_id": run_id, - "config": json.loads(cfg.to_json()), + "config": json.loads(orch_cfg.to_json()), }, indent=2, ) @@ -128,7 +149,10 @@ def cmd_run(argv: list[str]) -> int: # Spawn orchestrator and execute first-wins mp.set_start_method("spawn", force=True) orch = Orchestrator( - cfg, run_dir=run_dir, workers_dir=d["workers"], orchestrator_dir=orch_dir + orch_cfg, + run_dir=run_dir, + workers_dir=d["workers"], + orchestrator_dir=orch_dir, ) summary = orch.run() @@ -142,21 +166,5 @@ def cmd_run(argv: list[str]) -> int: return int(ExitCode.SUCCESS) -def main(argv: list[str] | None = None) -> int: - argv = list(sys.argv[1:] if argv is None else argv) - if not argv: - print( - "usage: fuse run --problem /abs/path.py [--model ] [flags]", - file=sys.stderr, - ) - return int(ExitCode.INVALID_ARGS) - cmd = argv[0] - if cmd == "run": - return cmd_run(argv[1:]) - else: - print(f"unknown subcommand: {cmd}", file=sys.stderr) - return int(ExitCode.INVALID_ARGS) - - if __name__ == "__main__": sys.exit(main()) diff --git a/Fuser/compose_end_to_end.py b/Fuser/compose_end_to_end.py index 02a3318..e8dbf18 100644 --- a/Fuser/compose_end_to_end.py +++ b/Fuser/compose_end_to_end.py @@ -23,31 +23,49 @@ `kernel_function(...)` and includes a minimal self-test that checks numerical equivalence against a PyTorch reference derived from the original problem. -Usage: +CLI (Hydra-based): python -m Fuser.compose_end_to_end \ - --problem /abs/path/to/kernelbench_problem.py \ - --subgraphs /abs/path/to/subgraphs.json \ - --kernels-summary /abs/path/to/kernels_out/summary.json \ - [--model gpt-5] [--out-dir ./compose_out] [--verify] - -Notes: -- Requires an available LLM provider configured via KernelAgent providers - (e.g., OPENAI_API_KEY for OpenAI models). -- Writes composed Python file to /composed_kernel.py and a -composition summary JSON. + problem=/abs/path/to/kernelbench_problem.py \ + subgraphs=/abs/path/to/subgraphs.json \ + kernels_summary=/abs/path/to/kernels_out/summary.json + + # Override config values: + python -m Fuser.compose_end_to_end \ + problem=/abs/path/to/problem.py \ + subgraphs=/abs/path/to/subgraphs.json \ + kernels_summary=/abs/path/to/summary.json \ + model=gpt-5 \ + out_dir=./compose_out \ + verify=true + + # Or use a custom config: + python -m Fuser.compose_end_to_end --config-name custom_compose \ + problem=/abs/path/to/problem.py \ + subgraphs=/abs/path/to/subgraphs.json \ + kernels_summary=/abs/path/to/summary.json + +Config file: configs/pipeline/compose_end_to_end.yaml + +Requirements: +- KernelAgent providers configured via environment + +Outputs: +- Composed Python file to /composed_kernel.py +- Composition summary JSON """ from __future__ import annotations -import argparse import json import os import textwrap from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Tuple from dotenv import load_dotenv +from hydra import main as hydra_main +from omegaconf import DictConfig # Reuse KernelAgent provider stack for LLM calls try: @@ -404,42 +422,19 @@ def compose( return result -def main(argv: Optional[List[str]] = None) -> int: +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="compose_end_to_end", +) +def main(cfg: DictConfig) -> int: load_dotenv() - p = argparse.ArgumentParser( - description="Compose end-to-end Triton kernel from subgraphs + generated kernels" - ) - p.add_argument( - "--problem", required=True, help="Absolute path to KernelBench problem file" - ) - p.add_argument( - "--subgraphs", required=True, help="Path to subgraphs.json from Fuser" - ) - p.add_argument( - "--kernels-summary", - required=True, - help="Path to summary.json from dispatch step", - ) - p.add_argument( - "--out-dir", - default="compose_out", - help="Output directory for composed artifacts", - ) - p.add_argument( - "--model", default=os.getenv("OPENAI_MODEL") or "gpt-5", help="LLM model name" - ) - p.add_argument( - "--verify", - action="store_true", - help="Execute generated file and check PASS sentinel", - ) - p.add_argument("--max-iters", type=int, default=5, help="Max LLM refinement rounds") - args = p.parse_args(argv) - problem_path = Path(args.problem).resolve() - subgraphs_path = Path(args.subgraphs).resolve() - kernels_summary_path = Path(args.kernels_summary).resolve() - out_dir = Path(args.out_dir).resolve() + model = cfg.model or os.getenv("OPENAI_MODEL") or "gpt-5" + problem_path = Path(cfg.problem).resolve() + subgraphs_path = Path(cfg.subgraphs).resolve() + kernels_summary_path = Path(cfg.kernels_summary).resolve() + out_dir = Path(cfg.out_dir).resolve() if not problem_path.is_file(): print(f"problem file not found: {problem_path}") @@ -457,9 +452,9 @@ def main(argv: Optional[List[str]] = None) -> int: subgraphs_path=subgraphs_path, kernels_summary_path=kernels_summary_path, out_dir=out_dir, - model_name=args.model, - verify=args.verify, - max_iters=args.max_iters, + model_name=model, + verify=cfg.verify, + max_iters=cfg.max_iters, ) print(json.dumps(res, indent=2)) return 0 diff --git a/Fuser/dispatch_kernel_agent.py b/Fuser/dispatch_kernel_agent.py index 89307f5..ff40446 100644 --- a/Fuser/dispatch_kernel_agent.py +++ b/Fuser/dispatch_kernel_agent.py @@ -23,9 +23,20 @@ - A per-subgraph directory containing the agent session artifacts - A summary JSON mapping subgraph ids to generation results -Usage: - python -m Fuser.dispatch_kernel_agent --subgraphs /abs/path/to/subgraphs.json \ - [--agent-model gpt-5] [--out-dir ./kernels_out] [--jobs 1] +CLI (Hydra-based): + python -m Fuser.dispatch_kernel_agent subgraphs=/abs/path/to/subgraphs.json + + # Override config values: + python -m Fuser.dispatch_kernel_agent subgraphs=/abs/path/to/subgraphs.json \ + agent_model=gpt-5 \ + out_dir=./kernels_out \ + jobs=2 + + # Or use a custom config: + python -m Fuser.dispatch_kernel_agent --config-name custom_dispatch \ + subgraphs=/abs/path/to/subgraphs.json + +Config file: configs/pipeline/dispatch_kernel_agent.yaml Requirements: - OPENAI_API_KEY (.env in CWD or environment) @@ -34,7 +45,6 @@ from __future__ import annotations -import argparse import json import os import textwrap @@ -43,6 +53,8 @@ import concurrent.futures as _futures from dotenv import load_dotenv +from hydra import main as hydra_main +from omegaconf import DictConfig try: from triton_kernel_agent import TritonKernelAgent @@ -406,39 +418,23 @@ def _handle_one(idx_item: Tuple[int, Dict[str, Any]]) -> Tuple[int, Dict[str, An return out_summary -def main(argv: List[str] | None = None) -> int: +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="dispatch_kernel_agent", +) +def main(cfg: DictConfig) -> int: load_dotenv() - p = argparse.ArgumentParser( - description="Generate Triton kernels for subgraphs via KernelAgent" - ) - p.add_argument( - "--subgraphs", required=True, help="Path to subgraphs.json produced by Fuser" - ) - p.add_argument( - "--out-dir", - default="kernels_out", - help="Output directory for per-subgraph artifacts", - ) - p.add_argument( - "--agent-model", default=None, help="Override KernelAgent model name (optional)" - ) - p.add_argument( - "--jobs", - type=str, - default="2", - help="Max concurrent subgraphs to dispatch (default: 2); use 'auto' to match subgraph count", - ) - args = p.parse_args(argv) - - subgraphs_path = Path(args.subgraphs).resolve() + subgraphs_path = Path(cfg.subgraphs).resolve() if not subgraphs_path.is_file(): print(f"subgraphs file not found: {subgraphs_path}", file=os.sys.stderr) return 2 - out_dir = Path(args.out_dir).resolve() + + out_dir = Path(cfg.out_dir).resolve() # Resolve jobs (support "auto") try: - if isinstance(args.jobs, str) and args.jobs.strip().lower() == "auto": + if isinstance(cfg.jobs, str) and cfg.jobs.strip().lower() == "auto": try: with subgraphs_path.open("r", encoding="utf-8") as f: _items = json.load(f) @@ -446,12 +442,15 @@ def main(argv: List[str] | None = None) -> int: except Exception: jobs_val = 1 else: - jobs_val = max(1, int(args.jobs)) + jobs_val = max(1, int(cfg.jobs)) except Exception: jobs_val = 1 summary_path = run( - subgraphs_path, out_dir, agent_model=args.agent_model, jobs=jobs_val + subgraphs_path, + out_dir, + agent_model=cfg.agent_model, + jobs=jobs_val, ) print(str(summary_path)) return 0 diff --git a/Fuser/pipeline.py b/Fuser/pipeline.py index 16633a3..c72fcfd 100644 --- a/Fuser/pipeline.py +++ b/Fuser/pipeline.py @@ -15,17 +15,27 @@ """ One-shot pipeline runner: extract → dispatch → compose. -Usage: - python -m Fuser.pipeline \ - --problem /abs/path/to/kernelbench_problem.py \ - --extract-model gpt-5 \ - --dispatch-model o4-mini \ - [--dispatch-jobs 1] \ - --compose-model o4-mini \ - --workers 4 --max-iters 5 \ - --llm-timeout-s 1200 --run-timeout-s 1200 \ - --out-root ./.fuse \ - [--verify] [--compose-max-iters 5] +CLI (Hydra-based): + python -m Fuser.pipeline problem=/abs/path/to/kernelbench_problem.py + + # Override config values: + python -m Fuser.pipeline problem=/abs/path/to/kernelbench_problem.py \ + extractor.model=gpt-5 \ + dispatcher.model=o4-mini \ + composer.model=o4-mini \ + extractor.workers=4 \ + extractor.max_iters=5 \ + extractor.llm_timeout_s=1200 \ + extractor.run_timeout_s=1200 \ + composer.verify=true \ + composer.max_iters=5 \ + dispatcher.jobs=2 + + # Or use a custom config: + python -m Fuser.pipeline --config-name custom_pipeline \ + problem=/abs/path/to/kernelbench_problem.py + +Config file: configs/pipeline/pipeline.yaml Writes all artifacts into the run directory created by the extractor. The final composed kernel and composition summary live under /compose_out. @@ -33,11 +43,13 @@ from __future__ import annotations -import argparse import json from pathlib import Path from typing import Optional +from hydra import main as hydra_main +from omegaconf import DictConfig + from .subgraph_extractor import extract_subgraphs_to_json from .dispatch_kernel_agent import run as dispatch_run from .compose_end_to_end import compose @@ -130,7 +142,12 @@ def run_pipeline( } -def main(argv: Optional[list[str]] = None) -> int: +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="pipeline", +) +def main(cfg: DictConfig) -> int: # Load .env if present for OPENAI_API_KEY, proxies, etc. try: from dotenv import load_dotenv # type: ignore @@ -138,33 +155,8 @@ def main(argv: Optional[list[str]] = None) -> int: load_dotenv() except Exception: pass - p = argparse.ArgumentParser( - description="End-to-end pipeline: extract → dispatch → compose" - ) - p.add_argument("--problem", required=True, help="Absolute path to the problem file") - p.add_argument("--extract-model", default="gpt-5") - p.add_argument( - "--dispatch-model", - default=None, - help="KernelAgent model (default: gpt-5 for level2 problems, else o4-mini)", - ) - p.add_argument( - "--dispatch-jobs", - type=str, - default="2", - help="Max concurrent KernelAgent subgraph tasks (default: 2); use 'auto' to match subgraph count", - ) - p.add_argument("--compose-model", default="o4-mini") - p.add_argument("--workers", type=int, default=4) - p.add_argument("--max-iters", type=int, default=5, help="Extractor iter budget") - p.add_argument("--llm-timeout-s", type=int, default=1200) - p.add_argument("--run-timeout-s", type=int, default=1200) - p.add_argument("--out-root", default=None) - p.add_argument("--verify", action="store_true") - p.add_argument("--compose-max-iters", type=int, default=5) - args = p.parse_args(argv) - - problem_path = Path(args.problem).resolve() + + problem_path = Path(cfg.problem).resolve() if not problem_path.is_file(): print(f"problem not found: {problem_path}") return 2 @@ -172,17 +164,17 @@ def main(argv: Optional[list[str]] = None) -> int: try: res = run_pipeline( problem_path=problem_path, - extract_model=args.extract_model, - dispatch_model=args.dispatch_model, - compose_model=args.compose_model, - dispatch_jobs=args.dispatch_jobs, - workers=args.workers, - max_iters=args.max_iters, - llm_timeout_s=args.llm_timeout_s, - run_timeout_s=args.run_timeout_s, - out_root=Path(args.out_root) if args.out_root else None, - verify=args.verify, - compose_max_iters=args.compose_max_iters, + extract_model=cfg.extractor.model, + dispatch_model=cfg.dispatcher.model, + compose_model=cfg.composer.model, + dispatch_jobs=cfg.dispatcher.jobs, + workers=cfg.extractor.workers, + max_iters=cfg.extractor.max_iters, + llm_timeout_s=cfg.extractor.llm_timeout_s, + run_timeout_s=cfg.extractor.run_timeout_s, + out_root=Path(cfg.out_root) if cfg.out_root else None, + verify=cfg.composer.verify, + compose_max_iters=cfg.composer.max_iters, ) print(json.dumps(res, indent=2)) return 0 diff --git a/Fuser/subgraph_extractor.py b/Fuser/subgraph_extractor.py index 96698bb..4ba5df7 100644 --- a/Fuser/subgraph_extractor.py +++ b/Fuser/subgraph_extractor.py @@ -17,22 +17,35 @@ model produced by Fuser. Produces a JSON array written to the run directory. Flow: -1) Run Fuser orchestrator against a KernelBench problem to produce code.py. -2) Ask the LLM to analyze the fused code and emit a JSON array of subgraphs with - exact input/output/weight shapes. Differences in shapes => different subgraph. +1) Run Fuser orchestrator against a KernelBench problem to produce + code.py. +2) Ask the LLM to analyze the fused code and emit a JSON array of + subgraphs with exact input/output/weight shapes. Differences in shapes + => different subgraph. 3) Parse the JSON from the LLM output, deduplicate by shape signature, and save to /subgraphs.json. -Usage: - python -m Fuser.subgraph_extractor --problem /abs/path.py [--model gpt-5] - [--workers 4] [--max-iters 5] [--llm-timeout-s 2400] [--run-timeout-s 2400] +CLI (Hydra-based): + python -m Fuser.subgraph_extractor problem=/abs/path/to/problem.py -This script loads .env in CWD (same behavior as Fuser CLI) for OPENAI_API_KEY. + # Override config values: + python -m Fuser.subgraph_extractor problem=/abs/path/to/problem.py \ + model=gpt-5 \ + workers=4 \ + max_iters=5 + + # Or use a custom config: + python -m Fuser.subgraph_extractor --config-name custom_subgraph_extractor \ + problem=/abs/path/to/problem.py + +Config file: configs/pipeline/subgraph_extractor.yaml + +Requirements: +- OPENAI_API_KEY (.env in CWD or environment) """ from __future__ import annotations -import argparse import json import re import sys @@ -46,6 +59,9 @@ from .paths import ensure_abs_regular_file, make_run_dirs, PathSafetyError from .event_adapter import EventAdapter +from hydra import main as hydra_main +from omegaconf import DictConfig + def _load_code_from_tar(artifact_path: Path) -> str: if not artifact_path.is_file(): @@ -345,34 +361,26 @@ def sort_w(obj: Any) -> Dict[str, Any]: return dirs["run_dir"], out_path -def main(argv: Optional[list[str]] = None) -> int: +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/pipeline"), + config_name="subgraph_extractor", +) +def main(cfg: DictConfig) -> int: _load_dotenv_if_present() - p = argparse.ArgumentParser( - description="Extract unique subgraphs with shapes (JSON)" - ) - p.add_argument( - "--problem", required=True, help="Absolute path to KernelBench problem file" - ) - p.add_argument("--model", default="gpt-5", help="OpenAI model name (Responses API)") - p.add_argument("--workers", type=int, default=4) - p.add_argument("--max-iters", type=int, default=5) - p.add_argument("--llm-timeout-s", type=int, default=2400) - p.add_argument("--run-timeout-s", type=int, default=2400) - args = p.parse_args(argv) - try: - problem_path = ensure_abs_regular_file(args.problem) + problem_path = ensure_abs_regular_file(cfg.problem) except PathSafetyError as e: print(str(e), file=sys.stderr) return 2 run_dir, json_path = extract_subgraphs_to_json( problem_path=problem_path, - model_name=args.model, - workers=args.workers, - max_iters=args.max_iters, - llm_timeout_s=args.llm_timeout_s, - run_timeout_s=args.run_timeout_s, + model_name=cfg.model, + workers=cfg.workers, + max_iters=cfg.max_iters, + llm_timeout_s=cfg.llm_timeout_s, + run_timeout_s=cfg.run_timeout_s, ) print(str(json_path)) return 0 diff --git a/configs/e2e_test.yaml b/configs/e2e_test.yaml new file mode 100644 index 0000000..881b706 --- /dev/null +++ b/configs/e2e_test.yaml @@ -0,0 +1,19 @@ +# Configuration for e2e_test.py +# End-to-end test harness for BF16 matmul+sigmoid kernel generation +# +# Note that the current config is meant to be a pass-through for .env + +# Number of parallel workers for verification +num_workers: null + +# Maximum refinement rounds per worker +max_rounds: null + +# Directory to write logs +log_dir: null + +# OpenAI model to use for generation +model_name: null + +# Whether to use high reasoning effort for OpenAI models +high_reasoning_effort: true diff --git a/configs/pipeline/auto_agent.yaml b/configs/pipeline/auto_agent.yaml new file mode 100644 index 0000000..47395d2 --- /dev/null +++ b/configs/pipeline/auto_agent.yaml @@ -0,0 +1,43 @@ +# Configuration for auto_agent.py +# Converts KernelBench problems either via KernelAgent or Fuser based on complexity + +problem: ??? # Required: absolute path to the problem file + +# KernelAgent settings +ka: + model_name: null # Optional; uses env default if omitted + num_workers: 4 + max_rounds: 10 + high_reasoning: true + +# Router LLM settings +router: + model: gpt-5 + high_reasoning: true + temperature: 0.2 + max_tokens: 700 + +# Fuser pipeline settings +fuser: + # Extraction step settings + extractor: + model: gpt-5 + workers: 4 + max_iters: 5 + llm_timeout_s: 1200 + run_timeout_s: 1200 + + # Dispatch step settings + dispatcher: + model: o4-mini + jobs: 2 # Max concurrent tasks; use 'auto' to match subgraph count + + # Composition step settings + composer: + model: o4-mini + verify: false + max_iters: 5 + +# Fallback and routing settings +routing: + allow_fallback: true diff --git a/configs/pipeline/compose_end_to_end.yaml b/configs/pipeline/compose_end_to_end.yaml new file mode 100644 index 0000000..27b2988 --- /dev/null +++ b/configs/pipeline/compose_end_to_end.yaml @@ -0,0 +1,18 @@ +# Configuration for compose_end_to_end.py +# Compose end-to-end Triton kernel from subgraphs + generated kernels + +problem: ??? # Required: absolute path to KernelBench problem file +subgraphs: ??? # Required: path to subgraphs.json from Fuser +kernels_summary: ??? # Required: path to summary.json from dispatch step + +# Output directory for composed artifacts +out_dir: compose_out + +# LLM configuration +model: null + +# Verification settings +verify: false # Execute generated file and check PASS sentinel + +# Refinement settings +max_iters: 5 diff --git a/configs/pipeline/dispatch_kernel_agent.yaml b/configs/pipeline/dispatch_kernel_agent.yaml new file mode 100644 index 0000000..5beed67 --- /dev/null +++ b/configs/pipeline/dispatch_kernel_agent.yaml @@ -0,0 +1,13 @@ +# Configuration for dispatch_kernel_agent.py +# Dispatches subgraphs to KernelAgent for Triton kernel generation + +subgraphs: ??? # Required: absolute path to subgraphs.json file + +# Output directory for per-subgraph artifacts +out_dir: kernels_out + +# KernelAgent model configuration +agent_model: null + +# Concurrency settings +jobs: 2 # Max concurrent subgraph dispatches; use 'auto' to match subgraph count diff --git a/configs/pipeline/orchestrator.yaml b/configs/pipeline/orchestrator.yaml new file mode 100644 index 0000000..1f934f2 --- /dev/null +++ b/configs/pipeline/orchestrator.yaml @@ -0,0 +1,26 @@ +# Configuration for cli.py (Fuser Orchestrator) +# Runs multiple workers concurrently to generate and verify fused code + +problem: ??? # Required: absolute path to the Python problem file + +# LLM configuration +model: gpt-5 + +# Worker configuration +workers: 4 +max_iters: 10 + +# Timeout settings +llm_timeout_s: 120 +run_timeout_s: 180 + +# Streaming configuration +stream: all # Stream mode: 'all', 'winner', or 'none' + +# Storage and isolation settings +store_responses: false # Store LLM responses to disk +isolated: false # Run workers in isolated environment +deny_network: false # Deny network access during execution + +# LLM features +enable_reasoning_extras: true # Enable reasoning extras in LLM prompts diff --git a/configs/pipeline/pipeline.yaml b/configs/pipeline/pipeline.yaml new file mode 100644 index 0000000..10914d6 --- /dev/null +++ b/configs/pipeline/pipeline.yaml @@ -0,0 +1,27 @@ +# Configuration for pipeline.py +# One-shot pipeline runner: extract → dispatch → compose +# Shares fuser configuration with auto_agent.py + +problem: ??? # Required: absolute path to the kernelbench problem file + +# Extraction step settings +extractor: + model: gpt-5 + workers: 4 + max_iters: 5 + llm_timeout_s: 1200 + run_timeout_s: 1200 + +# Dispatch step settings +dispatcher: + model: null + jobs: 2 # Max concurrent tasks; use 'auto' to match subgraph count + +# Composition step settings +composer: + model: o4-mini + verify: false + max_iters: 5 + +# Output settings (field unused) +out_root: null # Optional; if not set, uses default location diff --git a/configs/pipeline/subgraph_extractor.yaml b/configs/pipeline/subgraph_extractor.yaml new file mode 100644 index 0000000..d760f34 --- /dev/null +++ b/configs/pipeline/subgraph_extractor.yaml @@ -0,0 +1,15 @@ +# Configuration for subgraph_extractor.py +# Extract unique fusable subgraphs with shape signatures from fused models + +problem: ??? # Required: absolute path to KernelBench problem file + +# LLM configuration +model: gpt-5 # OpenAI model name (Responses API) + +# Orchestrator settings +workers: 4 # Number of concurrent workers +max_iters: 5 # Maximum iterations per worker + +# Timeout settings (in seconds) +llm_timeout_s: 2400 # LLM API timeout +run_timeout_s: 2400 # Overall run timeout diff --git a/configs/ui/fuser_ui.yaml b/configs/ui/fuser_ui.yaml new file mode 100644 index 0000000..1ef85f9 --- /dev/null +++ b/configs/ui/fuser_ui.yaml @@ -0,0 +1,18 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defaults: + - kernel_agent + +port: 8086 diff --git a/configs/ui/kernel_agent.yaml b/configs/ui/kernel_agent.yaml new file mode 100644 index 0000000..07fe782 --- /dev/null +++ b/configs/ui/kernel_agent.yaml @@ -0,0 +1,16 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +port: 8085 +host: localhost diff --git a/configs/ui/pipeline_ui.yaml b/configs/ui/pipeline_ui.yaml new file mode 100644 index 0000000..dd373dc --- /dev/null +++ b/configs/ui/pipeline_ui.yaml @@ -0,0 +1,18 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defaults: + - kernel_agent + +port: 8087 diff --git a/e2e_test.py b/e2e_test.py index 328de70..80b0d22 100644 --- a/e2e_test.py +++ b/e2e_test.py @@ -15,19 +15,33 @@ """End-to-end BF16 matmul+sigmoid test harness.""" +from pathlib import Path import sys import time from dotenv import load_dotenv +from hydra import main as hydra_main +from omegaconf import DictConfig from triton_kernel_agent import TritonKernelAgent -def main(): +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent / "configs"), + config_name="e2e_test", +) +def main(cfg: DictConfig) -> None: """Generate and test a BF16 matmul kernel with fused sigmoid activation.""" # Load environment load_dotenv() - # Create agent - agent = TritonKernelAgent() + # Create agent with config parameters + agent = TritonKernelAgent( + num_workers=cfg.num_workers, + max_rounds=cfg.max_rounds, + log_dir=cfg.log_dir, + model_name=cfg.model_name, + high_reasoning_effort=cfg.high_reasoning_effort, + ) print("=" * 80) print("BF16 Matmul with Fused Sigmoid Activation") diff --git a/pyproject.toml b/pyproject.toml index 37462ee..c402edd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ classifiers = [ ] dependencies = [ + "hydra-core", + "omegaconf", "openai", "anthropic", "jinja2", diff --git a/scripts/fuser_ui.py b/scripts/fuser_ui.py index c93c2dd..7bd80c4 100644 --- a/scripts/fuser_ui.py +++ b/scripts/fuser_ui.py @@ -16,8 +16,8 @@ from __future__ import annotations -import argparse import ast +import logging import os import tarfile import time @@ -27,6 +27,9 @@ from pathlib import Path from typing import List, Optional, Tuple +from hydra import main as hydra_main +from omegaconf import DictConfig + import gradio as gr from dotenv import load_dotenv @@ -41,6 +44,10 @@ from Fuser.orchestrator import Orchestrator from Fuser.paths import ensure_abs_regular_file, make_run_dirs, PathSafetyError +# Turn off noisy HTTPX logging +httpx_logger = logging.getLogger("httpx") +httpx_logger.setLevel(logging.WARNING) + @dataclass class RunArtifacts: @@ -865,14 +872,17 @@ def generate( return app -def main() -> None: - parser = argparse.ArgumentParser(description="FuserAgent UI") - parser.add_argument("--port", type=int, default=8086) - parser.add_argument("--host", type=str, default="localhost") - args = parser.parse_args() - +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/ui"), + config_name="fuser_ui", +) +def main(cfg: DictConfig) -> None: app = build_interface() + port = cfg.port + host = cfg.host + print("🚀 Starting FuserAgent UI...") meta_keyfile = Path("/var/facebook/x509_identities/server.pem") @@ -880,13 +890,13 @@ def main() -> None: if is_meta_devserver: server_name = os.uname()[1] - print(f"🌐 Meta devserver detected. Visit https://{server_name}:{args.port}/") + print(f"🌐 Meta devserver detected. Visit https://{server_name}:{port}/") print("💡 Ensure you're on the Meta VPN.") app.launch( share=False, show_error=True, server_name=server_name, - server_port=args.port, + server_port=port, ssl_keyfile=str(meta_keyfile), ssl_certfile=str(meta_keyfile), ssl_verify=False, @@ -894,12 +904,12 @@ def main() -> None: inbrowser=False, ) else: - print(f"🌐 Visit http://{args.host}:{args.port}/") + print(f"🌐 Visit http://{host}:{port}/") app.launch( share=False, show_error=True, - server_name=args.host, - server_port=args.port, + server_name=host, + server_port=port, show_api=False, inbrowser=True, ) diff --git a/scripts/pipeline_ui.py b/scripts/pipeline_ui.py index 3810ceb..7b02d88 100644 --- a/scripts/pipeline_ui.py +++ b/scripts/pipeline_ui.py @@ -16,7 +16,7 @@ from __future__ import annotations -import argparse +import logging import os import sys import time @@ -26,6 +26,9 @@ from pathlib import Path from typing import List, Optional, Tuple +from hydra import main as hydra_main +from omegaconf import DictConfig + import gradio as gr from dotenv import load_dotenv @@ -34,6 +37,10 @@ if str(_PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(_PROJECT_ROOT)) +# Turn off noisy HTTPX logging +httpx_logger = logging.getLogger("httpx") +httpx_logger.setLevel(logging.WARNING) + def _list_kernelbench_problems(base: Path) -> List[Tuple[str, str]]: """Return list of (label, absolute_path) pairs for KernelBench problems.""" @@ -690,15 +697,18 @@ def on_run( return app -def main() -> None: - parser = argparse.ArgumentParser(description="Pipeline UI") - parser.add_argument("--port", type=int, default=8087) - parser.add_argument("--host", type=str, default="localhost") - args = parser.parse_args() - +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/ui"), + config_name="pipeline_ui", +) +def main(cfg: DictConfig) -> None: load_dotenv() app = build_interface() + port = cfg.port + host = cfg.host + print("🚀 Starting Pipeline UI...") # Mirror fuser_ui devserver behavior for Meta VPN environments @@ -707,13 +717,13 @@ def main() -> None: if is_meta_devserver: server_name = os.uname()[1] - print(f"🌐 Meta devserver detected. Visit https://{server_name}:{args.port}/") + print(f"🌐 Meta devserver detected. Visit https://{server_name}:{port}/") print("💡 Ensure you're on the Meta VPN.") app.launch( share=False, show_error=True, server_name=server_name, - server_port=args.port, + server_port=port, ssl_keyfile=str(meta_keyfile), ssl_certfile=str(meta_keyfile), ssl_verify=False, @@ -721,12 +731,12 @@ def main() -> None: inbrowser=False, ) else: - print(f"🌐 Visit http://{args.host}:{args.port}/") + print(f"🌐 Visit http://{host}:{port}/") app.launch( share=False, show_error=True, - server_name=args.host, - server_port=args.port, + server_name=host, + server_port=port, show_api=False, inbrowser=True, ) diff --git a/scripts/triton_ui.py b/scripts/triton_ui.py index 48dd804..5d5e0b7 100644 --- a/scripts/triton_ui.py +++ b/scripts/triton_ui.py @@ -15,22 +15,28 @@ """Gradio UI for Triton Kernel Agent.""" -import argparse +import logging import os import time import traceback from pathlib import Path from typing import Any, Dict, Optional, Tuple +from hydra import main as hydra_main +from omegaconf import DictConfig + import gradio as gr from dotenv import load_dotenv - from triton_kernel_agent import TritonKernelAgent from triton_kernel_agent.providers.models import AVAILABLE_MODELS from triton_kernel_agent.providers.openai_provider import OpenAIProvider from triton_kernel_agent.providers.anthropic_provider import AnthropicProvider +# Turn off noisy HTTPX logging +httpx_logger = logging.getLogger("httpx") +httpx_logger.setLevel(logging.WARNING) + KERNELBENCH_BASE_PATH = ( Path(__file__).resolve().parent / "external" / "KernelBench" / "KernelBench" @@ -247,7 +253,7 @@ def _format_error_logs(self, result: Dict[str, Any], generation_time: float) -> """Format error logs for display""" logs = f"""## Generation Failed -**⏱️ Time:** {generation_time:.2f} seconds +**⏱️ Time:** {generation_time:.2f} seconds **❌ Error:** {result["message"]} **📁 Session:** `{os.path.basename(result["session_dir"])}` @@ -384,9 +390,9 @@ def _create_app() -> gr.Blocks: gr.Markdown( """ # 🚀 Triton Kernel Agent - + **AI-Powered GPU Kernel Generation** - + Generate optimized OpenAI Triton kernels from high-level descriptions. """ ) @@ -630,13 +636,13 @@ def handle_problem_select(evt: gr.SelectData): gr.Markdown( """ --- - + **💡 Tips:** - Be specific about input/output shapes and data types - - Include PyTorch equivalent code for reference + - Include PyTorch equivalent code for reference - Check the logs for detailed generation information - - **🔧 Configuration:** + + **🔧 Configuration:** - Provide your OpenAI or Anthropic API key above (not saved; session-only) - Or set the appropriate env var in `.env` (OPENAI_API_KEY or ANTHROPIC_API_KEY) - The key is only used for this session and automatically cleared @@ -646,14 +652,15 @@ def handle_problem_select(evt: gr.SelectData): return app -def main(): +@hydra_main( + version_base=None, + config_path=str(Path(__file__).resolve().parent.parent / "configs/ui"), + config_name="kernel_agent", +) +def main(cfg: DictConfig): """Create and launch the Gradio interface""" - parser = argparse.ArgumentParser(description="Triton Kernel Agent UI") - parser.add_argument("--port", type=int, default=8085, help="Port to run the UI on") - parser.add_argument("--host", type=str, default="localhost", help="Host to bind to") - args = parser.parse_args() - app = _create_app() + port = cfg.port # Check if running on Meta devserver (has Meta SSL certs) meta_keyfile = "/var/facebook/x509_identities/server.pem" @@ -665,14 +672,14 @@ def main(): if is_meta_devserver: # Meta devserver configuration server_name = os.uname()[1] # Get devserver hostname - print(f"🌐 Opening on Meta devserver: https://{server_name}:{args.port}/") + print(f"🌐 Opening on Meta devserver: https://{server_name}:{port}/") print("💡 Make sure you're connected to Meta VPN to access the demo") app.launch( share=False, show_error=True, server_name=server_name, - server_port=args.port, + server_port=port, ssl_keyfile=meta_keyfile, ssl_certfile=meta_keyfile, ssl_verify=False, @@ -681,16 +688,18 @@ def main(): ) else: # Local development configuration - print(f"🌐 Opening locally: http://{args.host}:{args.port}/") + host = cfg.host + + print(f"🌐 Opening locally: http://{host}:{port}/") print( - f"🚨 IMPORTANT: If Chrome shows blank page, try Safari: open -a Safari http://{args.host}:{args.port}/ 🚨" + f"🚨 IMPORTANT: If Chrome shows blank page, try Safari: open -a Safari http://{host}:{port}/ 🚨" ) app.launch( share=False, show_error=True, - server_name=args.host, - server_port=args.port, + server_name=host, + server_port=port, show_api=False, inbrowser=True, # Auto-open browser for local development ) diff --git a/tests/commands.md b/tests/commands.md new file mode 100644 index 0000000..abb4a7b --- /dev/null +++ b/tests/commands.md @@ -0,0 +1,29 @@ +## CLI Commands + +This doc provides a detailed list of commands available in the repo as well as how to custom them. + +### Pipeline Commands + +| Command | Basic | Override Config Example | Custom Config | +|---|---|---|---| +| **auto_agent** | `python -m Fuser.auto_agent problem=/path/to/problem.py` | `python -m Fuser.auto_agent problem=/path/to/problem.py ka.model=gpt-5 router.model=gpt-5 routing.allow_fallback=false` | `python -m Fuser.auto_agent --config-name custom_auto_agent problem=/path/to/problem.py` | +| **pipeline** | `python -m Fuser.pipeline problem=/path/to/problem.py` | `python -m Fuser.pipeline problem=/path/to/problem.py extractor.model=gpt-5 dispatcher.model=o4-mini composer.model=o4-mini` | `python -m Fuser.pipeline --config-name custom_pipeline problem=/path/to/problem.py` | +| **e2e_test** | `python e2e_test.py` | `python e2e_test.py num_workers=4 max_rounds=10 model_name=gpt-5 high_reasoning_effort=true` | `python e2e_test.py --config-name custom_e2e_test` | + + +#### Component Commands +| Command | Basic | Override Config Example | Custom Config | +|---|---|---|---| +| **cli** (orchestrator) | `python -m Fuser.cli problem=/path/to/problem.py` | `python -m Fuser.cli problem=/path/to/problem.py model=gpt-5 workers=4 max_iters=10 stream=winner` | `python -m Fuser.cli --config-name custom_fuser problem=/path/to/problem.py` | +| **subgraph_extractor** | `python -m Fuser.subgraph_extractor problem=/path/to/problem.py` | `python -m Fuser.subgraph_extractor problem=/path/to/problem.py model=gpt-5 workers=4 max_iters=5` | `python -m Fuser.subgraph_extractor --config-name custom_subgraph_extractor problem=/path/to/problem.py` | +| **dispatch_kernel_agent** | `python -m Fuser.dispatch_kernel_agent subgraphs=/path/to/subgraphs.json` | `python -m Fuser.dispatch_kernel_agent subgraphs=/path/to/subgraphs.json agent_model=gpt-5 out_dir=./kernels_out jobs=2` | `python -m Fuser.dispatch_kernel_agent --config-name custom_dispatch subgraphs=/path/to/subgraphs.json` | +| **compose_end_to_end** | `python -m Fuser.compose_end_to_end problem=/path/to/problem.py subgraphs=/path/to/subgraphs.json kernels_summary=/path/to/summary.json` | `python -m Fuser.compose_end_to_end problem=/path/to/problem.py subgraphs=/path/to/subgraphs.json kernels_summary=/path/to/summary.json model=gpt-5 verify=true` | `python -m Fuser.compose_end_to_end --config-name custom_compose problem=/path/to/problem.py subgraphs=/path/to/subgraphs.json kernels_summary=/path/to/summary.json` | + + +### UI Commands + +| UI | Entry Point (Default) | Entry Point (Custom Port) | Script (Default) | Script (Custom Port) | +|---|---|---|---|---| +| **kernel-agent** | `kernel-agent` | `kernel-agent port=8086` | `python scripts/kernel-agent.py` | `python scripts/kernel-agent.py port=8086` | +| **triton-ui** | `triton-ui` | `triton-ui port=8085` | `python scripts/triton_ui.py` | `python scripts/triton_ui.py port=8085` | +| **fuser-ui** | `fuser-ui` | `fuser-ui port=8084` | `python scripts/fuser_ui.py` | `python scripts/fuser_ui.py port=8084` |