From 362cff291c707ae4ac9ececdc0a757b19a1d1f96 Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Thu, 9 Apr 2026 14:49:00 +0200 Subject: [PATCH] feat: Add start_llama and start_loop CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added to rdagent CLI: - rdagent start_llama: Start llama.cpp server (replaces start_llama.sh) - rdagent start_loop: Start strategy generator loop (replaces start_strategy_loop.sh) Features: - start_llama: --model, --port, --gpu-layers, --ctx-size, --reasoning options - start_loop: --target, --max-wait options with auto-restart on crash - Both commands have full help (--help) and examples Moved .sh scripts to scripts/: - start_llama.sh → scripts/ (kept as reference) - start_strategy_loop.sh → scripts/ (kept as reference) Usage: rdagent start_llama # Start LLM server rdagent start_llama --gpu-layers 40 # Custom GPU layers rdagent start_loop # Start strategy loop rdagent start_loop --target 5 # Generate 5 strategies per run --- rdagent/app/cli.py | 219 ++++++++++++++++++ start_llama.sh => scripts/start_llama.sh | 0 .../start_strategy_loop.sh | 0 3 files changed, 219 insertions(+) rename start_llama.sh => scripts/start_llama.sh (100%) rename start_strategy_loop.sh => scripts/start_strategy_loop.sh (100%) diff --git a/rdagent/app/cli.py b/rdagent/app/cli.py index 8a971593..fd7fb8a8 100644 --- a/rdagent/app/cli.py +++ b/rdagent/app/cli.py @@ -1023,3 +1023,222 @@ def _generate_single_strategy_report(strategy_file: Path, output_dir: Path) -> D if __name__ == "__main__": app() + + +@app.command(name="start_llama") +def start_llama_cli( + model: str = typer.Option( + None, "--model", "-m", help="Path to model file" + ), + port: int = typer.Option(8081, "--port", "-p", help="Server port"), + gpu_layers: int = typer.Option(30, "--gpu-layers", "-g", help="GPU layers"), + ctx_size: int = typer.Option(80000, "--ctx-size", "-c", help="Context size"), + reasoning: bool = typer.Option(False, "--reasoning", help="Enable reasoning mode"), +): + """ + Start llama.cpp server for local LLM inference. + + Options: + --model/-m: Path to model file (default: from .env or ~/models/qwen3.5/) + --port/-p: Server port (default: 8081) + --gpu-layers/-g: GPU layers (default: 30) + --ctx-size/-c: Context size (default: 80000) + --reasoning: Enable reasoning mode (default: off) + + Examples: + rdagent start_llama + rdagent start_llama --gpu-layers 40 --ctx-size 4096 + rdagent start_llama --reasoning + """ + import subprocess + import sys + import os + + model_path = model or os.getenv( + "LLAMA_MODEL_PATH", + str(Path.home() / "models" / "qwen3.5" / "Qwen3.5-35B-A3B-Q3_K_M.gguf"), + ) + + llama_server = str(Path.home() / "llama.cpp" / "build" / "bin" / "llama-server") + + if not Path(llama_server).exists(): + print(f"❌ llama.cpp server not found: {llama_server}") + print("\nBuild it first:") + print(" cd ~/llama.cpp && mkdir -p build && cd build && cmake .. && make") + sys.exit(1) + + if not Path(model_path).exists(): + print(f"❌ Model not found: {model_path}") + sys.exit(1) + + cmd = [ + llama_server, + "--model", model_path, + "--n-gpu-layers", str(gpu_layers), + "--ctx-size", str(ctx_size), + "--port", str(port), + "--threads", "8", + "--threads-batch", "8", + "--parallel", "1", + "--flash-attn", + "--jinja", + "--host", "0.0.0.0", + ] + + if not reasoning: + cmd.extend(["--reasoning", "off"]) + + print(f"🚀 Starting llama.cpp server...") + print(f" Model: {Path(model_path).name}") + print(f" Port: {port}") + print(f" GPU Layers: {gpu_layers}") + print(f" Context: {ctx_size}") + print(f" Reasoning: {'on' if reasoning else 'off'}") + print() + + try: + os.execvp(cmd[0], cmd) + except Exception as e: + print(f"❌ Failed to start llama.cpp server: {e}") + sys.exit(1) + + +@app.command(name="start_loop") +def start_loop_cli( + target_count: int = typer.Option(3, "--target", "-t", help="Strategies per run"), + max_wait: int = typer.Option(1800, "--max-wait", "-w", help="Max wait per run (seconds)"), +): + """ + Start PREDIX strategy generator loop. + + Runs continuously, generating strategies with automatic restart on crash. + + Options: + --target/-t: Strategies to generate per run (default: 3) + --max-wait/-w: Max wait time per run in seconds (default: 1800 = 30min) + + Examples: + rdagent start_loop + rdagent start_loop --target 5 --max-wait 3600 + """ + import subprocess + import signal + import sys + import os + from datetime import datetime + import time + + script_dir = str(Path(__file__).parent.parent.parent.parent) + generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py" + logfile = f"{script_dir}/results/logs/generator_loop.log" + pidfile = "/tmp/predix_loop.pid" + + os.makedirs(f"{script_dir}/results/logs", exist_ok=True) + + def log(msg: str): + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + line = f"{ts} - {msg}" + print(line) + with open(logfile, "a") as f: + f.write(line + "\n") + + def cleanup(signum=None, frame=None): + log("Received termination signal. Cleaning up...") + try: + subprocess.run(["pkill", "-f", "predix_smart_strategy_gen.py"], capture_output=True) + except Exception: + pass + try: + os.remove(pidfile) + except FileNotFoundError: + pass + log("Cleanup complete. Exiting.") + sys.exit(0) + + signal.signal(signal.SIGTERM, cleanup) + signal.signal(signal.SIGINT, cleanup) + + with open(pidfile, "w") as f: + f.write(str(os.getpid())) + + log("=========================================") + log("🚀 PREDIX Generator Loop Starting") + log("=========================================") + log(f"Target: {target_count} strategies per run") + log(f"Max wait: {max_wait}s per run") + log(f"Log: {logfile}") + + attempt = 0 + + while True: + attempt += 1 + log("") + log(f"=== Attempt #{attempt} ===================================") + + # Check disk space + try: + usage = subprocess.run(["df", "-h", script_dir], capture_output=True, text=True) + disk_line = usage.stdout.strip().split("\n")[-1] + pct = int(disk_line.split()[4].replace("%", "")) + if pct > 90: + log(f"⚠️ Disk usage at {pct}%. Pausing...") + time.sleep(300) + continue + except Exception: + pass + + # Count existing strategies + from pathlib import Path as P + strat_dir = P(f"{script_dir}/results/strategies_new") + strat_count = len(list(strat_dir.glob("*.json"))) if strat_dir.exists() else 0 + log(f"📁 Existing strategies: {strat_count}") + + # Kill stale processes + try: + subprocess.run(["pkill", "-9", "-f", "predix_smart_strategy_gen.py"], capture_output=True) + except Exception: + pass + time.sleep(2) + + # Start generator + log("🤖 Starting generator...") + proc = subprocess.Popen( + generator.split(), + cwd=script_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + log(f" PID: {proc.pid}") + + # Monitor progress + elapsed = 0 + while proc.poll() is None: + time.sleep(30) + elapsed += 30 + + if elapsed % 120 == 0: + log(f" ⏱️ {elapsed}s elapsed") + + if elapsed >= max_wait: + log(f" ⏰ Timeout after {elapsed}s. Killing...") + proc.kill() + break + + # Check results + exit_code = proc.wait() + if exit_code == 0: + log("✅ Generator completed successfully") + elif exit_code == -9: + log("❌ Generator killed (OOM? Exit 137)") + else: + log(f"⚠️ Generator exited with code {exit_code}") + + # Count new strategies + new_strats = sorted(strat_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)[:3] + if new_strats: + log("📊 Latest strategies:") + for s in new_strats: + log(f" - {s.name}") + + log("⏳ Waiting 60s before next attempt...") + time.sleep(60) diff --git a/start_llama.sh b/scripts/start_llama.sh similarity index 100% rename from start_llama.sh rename to scripts/start_llama.sh diff --git a/start_strategy_loop.sh b/scripts/start_strategy_loop.sh similarity index 100% rename from start_strategy_loop.sh rename to scripts/start_strategy_loop.sh