compiler-env / inference.py
HideIron's picture
Upload folder using huggingface_hub
d4d59cd verified
Raw History Blame Contribute Delete
20.4 kB
"""
Inference Script for IRis Compiler Optimization Environment
===================================
MANDATORY
- Before submitting, ensure the following variables are defined in your environment configuration:
API_BASE_URL The API endpoint for the LLM.
MODEL_NAME The model identifier to use for inference.
HF_TOKEN Your Hugging Face / API key.
LOCAL_IMAGE_NAME The name of the local image to use for the environment if you are using from_docker_image()
method
- Defaults are set only for API_BASE_URL and MODEL_NAME
(and should reflect your active inference setup):
API_BASE_URL = os.getenv("API_BASE_URL", "<your-active-endpoint>")
MODEL_NAME = os.getenv("MODEL_NAME", "<your-active-model>")
- The inference script must be named inference.py and placed in the root directory of the project
- Participants must use OpenAI Client for all LLM calls using above variables
STDOUT FORMAT
- The script must emit exactly three line types to stdout, in this order:
[START] task=<task_name> env=<benchmark> model=<model_name>
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
Rules:
- One [START] line at episode begin.
- One [STEP] line per step, immediately after env.step() returns.
- One [END] line after env.close(), always emitted (even on exception).
- reward and rewards are formatted to 2 decimal places.
- done and success are lowercase booleans: true or false.
- error is the raw last_action_error string, or null if none.
- All fields on a single line with no newlines within a line.
- Each tasks should return score in [0, 1]
"""
import asyncio
import json
import os
import sys
import textwrap
from typing import List, Optional
from openai import OpenAI
try:
from compiler_env import CompilerAction, CompilerEnv
except ImportError:
# Running directly from repo root (not installed as package)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from client import CompilerEnv
from models import CompilerAction
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
HF_TOKEN = os.getenv("HF_TOKEN")
MEMORY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "agent_memory.json")
MAX_GOOD_SEQUENCES = 10
MAX_BAD_SEQUENCES = 10
MAX_INSIGHTS = 15
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
BENCHMARK = os.getenv("COMPILER_ENV_BENCHMARK", "compiler_env")
MAX_STEPS = 30
TEMPERATURE = 0.7
MAX_TOKENS = 512
SUCCESS_SCORE_THRESHOLD = 0.1
# Max possible reward: 0.1 * 19 (all unique passes improve) + 1.0 (beat O3) = 2.9
MAX_TOTAL_REWARD = 2.9
SYSTEM_PROMPT = textwrap.dedent(
"""
You are an expert LLVM compiler optimization agent. Your goal is to select
LLVM optimization passes that minimize the execution time of a C program
compiled for RISC-V architecture.
IMPORTANT: Each add_pass is automatically compiled and measured.
- If the pass IMPROVES execution time -> it is KEPT in the sequence (+0.1 reward)
- If it does NOT improve -> it is AUTO-REMOVED (-0.05 reward)
- Duplicate passes are REJECTED (-0.1 reward)
You get immediate feedback after each add_pass showing the measured time.
Each turn you must reply with exactly ONE tool call. Available tools:
- add_pass(pass_name): Try adding a pass. Auto-compiled and evaluated.
- remove_pass(pass_name): Remove a previously kept pass (-0.05 penalty).
- reorder_sequence(new_order): Reorder passes. +0.1 if improves, -0.05 if not (reverts).
- finalize(): End the episode. You receive a final bonus based on baselines.
Available 19 passes (use exact names):
MEMORY:
mem2reg — Promotes stack allocations to SSA registers (foundational, apply first)
sroa — Scalar Replacement of Aggregates: breaks structs into scalars
CFG:
simplifycfg — Simplifies control flow: merges blocks, removes dead branches
SCALAR:
instcombine — Peephole optimizations: combines/simplifies instructions
gvn — Global Value Numbering: eliminates redundant computations
sccp — Sparse Conditional Constant Propagation: folds constants
dce — Dead Code Elimination: removes unused instructions
dse — Dead Store Elimination: removes writes never read
early-cse — Common Subexpression Elimination: removes duplicate expressions
reassociate — Reassociates arithmetic for better constant folding
LOOP:
loop-simplify — Canonicalizes loops into standard form (prerequisite for other loop opts)
loop-rotate — Rotates loops to do-while form (enables better unrolling)
loop-unroll — Unrolls loops to reduce branch overhead (big perf impact)
licm — Loop Invariant Code Motion: hoists invariant code out of loops
indvars — Simplifies induction variables in loops
loop-reduce — Loop Strength Reduction: replaces expensive ops (multiply→add)
INLINE:
inline — Inlines function calls to eliminate call overhead
RISC-V:
consthoist — Hoists expensive constants to reduce register pressure
div-rem-pairs — Combines division+remainder into single operation
Strategy:
1. Start with foundational passes: mem2reg, sroa, simplifycfg, instcombine
2. Add scalar optimizations: gvn, dce, dse, early-cse, sccp, reassociate
3. Add loop optimizations: loop-simplify, loop-rotate, licm, indvars, loop-unroll, loop-reduce
4. Try inlining: inline
5. Try RISC-V specific: consthoist, div-rem-pairs
6. Read the feedback after each add_pass to understand what helps
7. Call finalize() when you've tried enough passes or are satisfied
8. Order matters: mem2reg should come before most other passes
9. Do NOT add the same pass twice — duplicates are penalized
"""
).strip()
TOOLS = [
{
"type": "function",
"function": {
"name": "add_pass",
"description": "Add an LLVM optimization pass to the current sequence.",
"parameters": {
"type": "object",
"properties": {
"pass_name": {
"type": "string",
"description": "Name of the LLVM pass (e.g. 'mem2reg', 'licm', 'gvn')",
}
},
"required": ["pass_name"],
},
},
},
{
"type": "function",
"function": {
"name": "remove_pass",
"description": "Remove a previously kept pass from the sequence. Use this to undo a bad pass choice. Costs -0.05 reward.",
"parameters": {
"type": "object",
"properties": {
"pass_name": {
"type": "string",
"description": "Name of the LLVM pass to remove from the kept sequence",
}
},
"required": ["pass_name"],
},
},
},
{
"type": "function",
"function": {
"name": "reorder_sequence",
"description": "Reorder the current pass sequence. Provide the same passes in a new order. Auto-compiles: +0.1 if time improves (keeps new order), -0.05 if not (reverts).",
"parameters": {
"type": "object",
"properties": {
"new_order": {
"type": "string",
"description": "Comma-separated pass names in the desired new order, e.g. 'gvn,mem2reg,simplifycfg'",
}
},
"required": ["new_order"],
},
},
},
{
"type": "function",
"function": {
"name": "finalize",
"description": "End the episode. Computes final bonus based on how your best time compares to O0/O1/O2/O3 baselines. Call this when you are satisfied with your pass sequence.",
"parameters": {"type": "object", "properties": {}},
},
},
]
# ------------------------------------------------------------------
# Agent Memory
# ------------------------------------------------------------------
def load_memory() -> dict:
"""Load agent memory from JSON file."""
default = {"good_sequences": [], "bad_sequences": [], "insights": []}
try:
with open(MEMORY_FILE, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return default
def save_memory(memory: dict) -> None:
"""Save agent memory to JSON file."""
try:
with open(MEMORY_FILE, "w") as f:
json.dump(memory, f, indent=2)
except Exception as e:
print(f"[DEBUG] Failed to save memory: {e}", flush=True)
def format_memory_for_prompt(memory: dict) -> str:
"""Format memory as a readable string for the LLM system prompt."""
parts = []
if memory.get("good_sequences"):
parts.append("GOOD SEQUENCES (these worked well before):")
for entry in memory["good_sequences"][-5:]:
seq = ", ".join(entry["sequence"])
parts.append(f" - [{seq}] improvement={entry.get('improvement', '?')} program={entry.get('program_type', '?')}")
if memory.get("bad_sequences"):
parts.append("BAD SEQUENCES (avoid these):")
for entry in memory["bad_sequences"][-5:]:
seq = ", ".join(entry["sequence"])
parts.append(f" - [{seq}] reason={entry.get('reason', '?')}")
if memory.get("insights"):
parts.append("INSIGHTS FROM PREVIOUS EPISODES:")
for insight in memory["insights"][-5:]:
parts.append(f" - {insight}")
return "\n".join(parts) if parts else "No previous experience yet."
def update_memory(
memory: dict,
program_name: str,
kept_passes: List[str],
removed_passes: List[str],
best_time: float,
baseline_O0: float,
tier_beaten: str,
) -> dict:
"""Update agent memory after an episode."""
# Compute improvement percentage
if baseline_O0 and baseline_O0 > 0 and best_time < baseline_O0:
improvement_pct = ((baseline_O0 - best_time) / baseline_O0) * 100
improvement_str = f"+{improvement_pct:.1f}%"
else:
improvement_str = "0%"
# Add good sequence if any passes were kept
if kept_passes:
memory["good_sequences"].append({
"sequence": kept_passes,
"improvement": improvement_str,
"program_type": program_name,
})
# Trim to max size
memory["good_sequences"] = memory["good_sequences"][-MAX_GOOD_SEQUENCES:]
# Add bad sequences (passes that were removed)
if removed_passes:
memory["bad_sequences"].append({
"sequence": removed_passes,
"reason": f"No improvement on {program_name}",
})
memory["bad_sequences"] = memory["bad_sequences"][-MAX_BAD_SEQUENCES:]
# Auto-generate insights
if kept_passes and tier_beaten != "none":
first_pass = kept_passes[0]
memory["insights"].append(
f"Starting with {first_pass} worked well on {program_name} (beat {tier_beaten})"
)
if len(kept_passes) == 0:
memory["insights"].append(
f"No passes helped on {program_name} — may need different approach"
)
memory["insights"] = memory["insights"][-MAX_INSIGHTS:]
return memory
def log_start(task: str, env: str, model: str) -> None:
print(f"[START] task={task} env={env} model={model}", flush=True)
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
error_val = error if error else "null"
done_val = str(done).lower()
print(
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
flush=True,
)
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
def tool_call_to_action(tool_name: str, tool_args: dict) -> str:
"""Convert an OpenAI tool call into an environment action string."""
if tool_name == "add_pass":
return f"add_pass:{tool_args.get('pass_name', '')}"
if tool_name == "remove_pass":
return f"remove_pass:{tool_args.get('pass_name', '')}"
if tool_name == "reorder_sequence":
return f"reorder_sequence:{tool_args.get('new_order', '')}"
return tool_name
def get_model_tool_call(client: OpenAI, messages: list) -> tuple:
"""Ask the LLM for its next tool call. Returns (tool_name, tool_args, raw_tool_call)."""
try:
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=TOOLS,
tool_choice="auto",
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
stream=False,
)
msg = completion.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return None, None, msg
tc = msg.tool_calls[0]
fn_name = tc.function.name
fn_args = json.loads(tc.function.arguments) if tc.function.arguments else {}
return fn_name, fn_args, tc
except Exception as exc:
print(f"[DEBUG] Model request failed: {exc}", flush=True)
return None, None, None
TASK_IDS = ["easy", "medium", "hard"]
async def create_env() -> CompilerEnv:
"""Create environment client, trying multiple connection methods."""
# 1. If an explicit URL is provided, connect directly
env_url = os.getenv("OPENENV_URL") or os.getenv("ENV_URL")
if env_url:
print(f"[DEBUG] Connecting to env URL: {env_url}", flush=True)
client = CompilerEnv(base_url=env_url)
await client.connect()
return client
# 2. If LOCAL_IMAGE_NAME is set, start from Docker image
if LOCAL_IMAGE_NAME:
print(f"[DEBUG] Starting from Docker image: {LOCAL_IMAGE_NAME}", flush=True)
return await CompilerEnv.from_docker_image(LOCAL_IMAGE_NAME)
# 3. Fallback: try localhost:8000 (container already running)
print("[DEBUG] No image/URL set, trying localhost:8000", flush=True)
client = CompilerEnv(base_url="http://localhost:8000")
await client.connect()
return client
async def run_task(
task_id: str, env: CompilerEnv, llm_client: OpenAI, memory: dict
) -> dict:
"""Run a single task (one [START]...[END] block). Returns updated memory."""
rewards: List[float] = []
steps_taken = 0
score = 0.001
success = False
kept_passes: List[str] = []
removed_passes: List[str] = []
program_name = "unknown"
baseline_O0 = 0.0
best_time = 0.0
tier_beaten = "none"
log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
try:
result = await env.reset(task_id=task_id)
obs = result.observation
obs_data = obs.data
program_name = obs_data.get("program", "unknown")
baseline_O0 = obs_data.get("baselines", {}).get("O0", 0.0)
best_time = obs_data.get("best_time", baseline_O0)
# Format memory context for the LLM
memory_context = format_memory_for_prompt(memory)
# Build initial conversation with program info + memory
history_messages: list = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": (
f"You are optimizing: {program_name}\n"
f"Baseline execution times:\n"
f" O0: {obs_data.get('baselines', {}).get('O0', '?')}s\n"
f" O1: {obs_data.get('baselines', {}).get('O1', '?')}s\n"
f" O2: {obs_data.get('baselines', {}).get('O2', '?')}s\n"
f" O3: {obs_data.get('baselines', {}).get('O3', '?')}s\n"
f"Current best time: {obs_data.get('best_time', '?')}s (starts at O0)\n"
f"Steps remaining: {obs_data.get('steps_remaining', MAX_STEPS)}\n\n"
f"--- MEMORY FROM PREVIOUS EPISODES ---\n"
f"{memory_context}\n"
f"--- END MEMORY ---\n\n"
f"Add passes one at a time. Each is auto-compiled and measured.\n"
f"Only passes that improve execution time are kept.\n"
f"Use insights from memory to guide your choices.\n"
f"Call finalize() when satisfied."
),
},
]
for step in range(1, MAX_STEPS + 1):
if result.done:
break
# Ask LLM for next tool call
fn_name, fn_args, raw_tc = get_model_tool_call(llm_client, history_messages)
if fn_name is None:
# No tool call -- agent stopped; force finalize
action_str = "finalize"
else:
action_str = tool_call_to_action(fn_name, fn_args)
# Step the environment
result = await env.step(CompilerAction(action=action_str))
obs = result.observation
reward = result.reward or 0.0
done = result.done
error = obs.message if obs.status in ("error", "invalid_pass", "compile_failed", "duplicate") else None
# Track kept/removed passes for memory
if obs.status == "pass_kept":
kept_passes.append(obs.data.get("pass", ""))
best_time = obs.data.get("best_time", best_time)
elif obs.status == "pass_removed":
removed_passes.append(obs.data.get("pass", ""))
elif obs.status == "finalized":
tier_beaten = obs.data.get("tier_beaten", "none")
best_time = obs.data.get("final_time", best_time)
rewards.append(reward)
steps_taken = step
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
# Feed tool result back to LLM conversation
if raw_tc and hasattr(raw_tc, "id"):
tool_result_str = json.dumps({
"status": obs.status,
"message": obs.message,
"data": obs.data,
"reward": reward,
"done": done,
})
history_messages.append({
"role": "tool",
"tool_call_id": raw_tc.id,
"content": tool_result_str,
})
if done:
break
# Compute score: normalize cumulative reward to [0, 1]
total_reward = rewards[-1] if rewards else 0.0 # last reward is cumulative
score = total_reward / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
score = min(max(score, 0.001), 0.999)
success = score >= SUCCESS_SCORE_THRESHOLD
except Exception as exc:
print(f"[DEBUG] Task {task_id} error: {exc}", flush=True)
finally:
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
# Update memory with episode results
memory = update_memory(
memory, program_name, kept_passes, removed_passes,
best_time, baseline_O0, tier_beaten,
)
return memory
async def main() -> None:
llm_client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
env = await create_env()
memory = load_memory()
try:
for task_id in TASK_IDS:
memory = await run_task(task_id, env, llm_client, memory)
save_memory(memory)
finally:
try:
await env.close()
except Exception as e:
print(f"[DEBUG] env.close() error (container cleanup): {e}", flush=True)
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as exc:
print(f"[END] success=false steps=0 score=0.001 rewards=", flush=True)
print(f"[DEBUG] Fatal error: {exc}", flush=True)
sys.exit(1)