Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +81 -0
- README.md +317 -4
- __init__.py +16 -0
- baseline_agent.py +233 -0
- client.py +79 -0
- models.py +125 -0
- openenv.yaml +7 -0
- pyproject.toml +34 -0
- server/__init__.py +11 -0
- server/app.py +43 -0
- server/compiler_opt_env_environment.py +402 -0
- server/requirements.txt +6 -0
- test_env.py +97 -0
Dockerfile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=compiler_opt_env
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 81 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,323 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Compiler Pass Ordering RL Environment
|
| 3 |
+
emoji: ⚙️
|
| 4 |
+
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Compiler Pass Ordering RL Environment
|
| 15 |
+
|
| 16 |
+
An OpenEnv environment that simulates **compiler optimization pass ordering** — a real-world task performed by production compilers (GCC, LLVM) every time they build software. The agent must select a sequence of optimization passes to apply to a program's Intermediate Representation (IR) to minimize estimated runtime cost.
|
| 17 |
+
|
| 18 |
+
This is the same class of problem addressed by DeepMind's MLGO paper (2022), which showed RL finds better pass orderings than the hand-tuned heuristics used in production compilers today.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## Motivation
|
| 23 |
+
|
| 24 |
+
When a compiler optimizes code, it applies a sequence of transformations called **passes**: dead code elimination, loop unrolling, vectorization, function inlining, etc. There are ~50 such passes in LLVM. The order you apply them matters enormously — applying pass A before B can unlock optimizations that B before A cannot.
|
| 25 |
+
|
| 26 |
+
**Why RL is necessary here (not greedy or heuristics):**
|
| 27 |
+
|
| 28 |
+
- Greedy selection (always pick the pass with highest immediate benefit) achieves ~20% cost reduction.
|
| 29 |
+
- An RL agent that learns prerequisite chains (e.g., `alias_analysis → vectorization` gives 7x effectiveness) achieves ~42-50%.
|
| 30 |
+
- The search space is factorial (15! orderings), and the reward for an "enabling" pass only materializes 3-4 steps later — exactly the credit assignment problem RL is built for.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Environment Design
|
| 35 |
+
|
| 36 |
+
### Prerequisite Gates
|
| 37 |
+
|
| 38 |
+
The core mechanism that makes RL necessary. Each high-value pass has prerequisites:
|
| 39 |
+
|
| 40 |
+
| Pass | Prerequisites Required | Synergy Multiplier | Without Prerequisites |
|
| 41 |
+
|---|---|---|---|
|
| 42 |
+
| vectorization | alias_analysis + dead_code_elimination | 7.0x | 0.3x (penalty) |
|
| 43 |
+
| function_inlining | interprocedural_analysis | 5.0x | 0.3x |
|
| 44 |
+
| memory_coalescing | alias_analysis | 3.5x | 0.3x |
|
| 45 |
+
| strength_reduction | alias_analysis + constant_folding | 3.0x | 0.3x |
|
| 46 |
+
| instruction_scheduling | register_allocation | 2.5x | 0.3x |
|
| 47 |
+
| loop_unrolling | loop_invariant_motion | 2.0x | 0.3x |
|
| 48 |
+
|
| 49 |
+
A greedy agent never applies `alias_analysis` (base effect = 0.01) and so never unlocks `vectorization`. An RL agent learns to sacrifice early reward to enable these chains.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## Action Space
|
| 54 |
+
|
| 55 |
+
```python
|
| 56 |
+
class CompilerOptAction(Action):
|
| 57 |
+
pass_id: int # Integer in [0, 14] — which pass to apply
|
| 58 |
+
task_id: int # 1 = easy, 2 = medium, 3 = hard
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
**Available passes:**
|
| 62 |
+
|
| 63 |
+
| ID | Pass Name | Base Effect | Role |
|
| 64 |
+
|---|---|---|---|
|
| 65 |
+
| 0 | dead_code_elimination | 0.08 | Enabler for vectorization |
|
| 66 |
+
| 1 | constant_folding | 0.06 | Enabler for strength_reduction |
|
| 67 |
+
| 2 | loop_unrolling | 0.10 | High value, needs LIM first |
|
| 68 |
+
| 3 | function_inlining | 0.03 | High potential, needs interproc |
|
| 69 |
+
| 4 | vectorization | 0.02 | Highest potential (7x with prereqs) |
|
| 70 |
+
| 5 | loop_invariant_motion | 0.06 | Enabler for loop_unrolling |
|
| 71 |
+
| 6 | strength_reduction | 0.05 | Good with alias + const_fold |
|
| 72 |
+
| 7 | common_subexpr_elimination | 0.07 | Standalone value |
|
| 73 |
+
| 8 | tail_call_optimization | 0.04 | Standalone value |
|
| 74 |
+
| 9 | branch_prediction_hints | 0.03 | Standalone value |
|
| 75 |
+
| 10 | register_allocation | 0.05 | Enabler for instr_scheduling |
|
| 76 |
+
| 11 | instruction_scheduling | 0.04 | Needs reg_alloc first |
|
| 77 |
+
| 12 | memory_coalescing | 0.06 | Needs alias first |
|
| 78 |
+
| 13 | alias_analysis | 0.01 | **Key enabler** — low base, high unlock |
|
| 79 |
+
| 14 | interprocedural_analysis | 0.01 | **Key enabler** — low base, high unlock |
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## Observation Space
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
class CompilerOptObservation(Observation):
|
| 87 |
+
# Cost tracking
|
| 88 |
+
estimated_cost: float # Current runtime cost estimate
|
| 89 |
+
baseline_cost: float # Cost before any optimization
|
| 90 |
+
|
| 91 |
+
# Program IR features (static per episode)
|
| 92 |
+
num_instructions: int
|
| 93 |
+
num_loops: int
|
| 94 |
+
num_branches: int
|
| 95 |
+
num_functions: int
|
| 96 |
+
loop_depth: int
|
| 97 |
+
program_type: str # e.g. "vectorizable", "loop_heavy"
|
| 98 |
+
|
| 99 |
+
# Episode progress
|
| 100 |
+
passes_applied: List[int] # Ordered history of applied passes
|
| 101 |
+
passes_available: List[int]
|
| 102 |
+
step_count: int
|
| 103 |
+
max_steps: int # 10
|
| 104 |
+
|
| 105 |
+
# Synergy state
|
| 106 |
+
synergy_state: List[float] # Per-pass current effectiveness multiplier
|
| 107 |
+
|
| 108 |
+
# Task info
|
| 109 |
+
task_id: int
|
| 110 |
+
task_description: str
|
| 111 |
+
|
| 112 |
+
# Results
|
| 113 |
+
done: bool
|
| 114 |
+
reward: float
|
| 115 |
+
improvement_pct: float # Total % cost reduction from baseline
|
| 116 |
+
last_pass_name: str
|
| 117 |
+
grader_score: float # 0.0–1.0, populated when done=True
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## Tasks
|
| 123 |
+
|
| 124 |
+
### Task 1 — Easy
|
| 125 |
+
**Goal:** Optimize a vectorizable or loop-heavy program by discovering the primary synergy chain.
|
| 126 |
+
|
| 127 |
+
**What the agent must learn:** Apply `alias_analysis` (low immediate value) then `dead_code_elimination`, then `vectorization` fires at 7x base effectiveness.
|
| 128 |
+
|
| 129 |
+
**Grader thresholds:**
|
| 130 |
+
- Score 1.0: ≥35% cost reduction
|
| 131 |
+
- Score 0.7: ≥25% cost reduction
|
| 132 |
+
- Score 0.4: ≥15% cost reduction
|
| 133 |
+
- Score 0.0: <15%
|
| 134 |
+
|
| 135 |
+
**Greedy baseline:** ~18-20% | **RL target:** ~38-45%
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
### Task 2 — Medium
|
| 140 |
+
**Goal:** Optimize a compute-heavy or inlining-heavy program by discovering **two independent synergy chains**.
|
| 141 |
+
|
| 142 |
+
**What the agent must learn:**
|
| 143 |
+
- Chain 1: `alias_analysis + DCE → vectorization` (7x)
|
| 144 |
+
- Chain 2: `interprocedural_analysis → function_inlining` (5x)
|
| 145 |
+
Both chains must be activated within 10 steps.
|
| 146 |
+
|
| 147 |
+
**Grader thresholds:**
|
| 148 |
+
- Score 1.0: ≥45% cost reduction
|
| 149 |
+
- Score 0.7: ≥35% cost reduction
|
| 150 |
+
- Score 0.4: ≥22% cost reduction
|
| 151 |
+
- Score 0.0: <22%
|
| 152 |
+
|
| 153 |
+
**Greedy baseline:** ~20-24% | **RL target:** ~42-50%
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
### Task 3 — Hard
|
| 158 |
+
**Goal:** Optimize any program type. Program is sampled randomly from 7 templates. The agent must generalize across program types with different optimal orderings.
|
| 159 |
+
|
| 160 |
+
**What the agent must learn:** Generalized pass ordering policy. Different programs reward different chains. A `vectorizable` program rewards chain 1; a `recursive` program rewards chain 2; a `loop_heavy` program rewards `LIM → loop_unrolling`.
|
| 161 |
+
|
| 162 |
+
**Grader thresholds:**
|
| 163 |
+
- Score 1.0: ≥52% cost reduction
|
| 164 |
+
- Score 0.7: ≥42% cost reduction
|
| 165 |
+
- Score 0.4: ≥28% cost reduction
|
| 166 |
+
- Score 0.0: <28%
|
| 167 |
+
|
| 168 |
+
**Greedy baseline:** ~19-23% | **RL target:** ~45-52%
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## Reward Function
|
| 173 |
+
|
| 174 |
+
Reward is provided at every step (not just terminal), giving a dense signal across the full trajectory:
|
| 175 |
+
|
| 176 |
+
```
|
| 177 |
+
Per-step reward = marginal_improvement - 0.02 (step penalty)
|
| 178 |
+
|
| 179 |
+
marginal_improvement = (cost_before - cost_after) / baseline_cost
|
| 180 |
+
|
| 181 |
+
Terminal bonus:
|
| 182 |
+
improvement >= 50%: +0.6
|
| 183 |
+
improvement >= 40%: +0.4
|
| 184 |
+
improvement >= 30%: +0.2
|
| 185 |
+
improvement >= 20%: +0.05
|
| 186 |
+
improvement <= 0%: -0.2
|
| 187 |
+
|
| 188 |
+
Penalties:
|
| 189 |
+
Re-applying an already-used pass: -0.3
|
| 190 |
+
Applying pass without prerequisites: reward naturally low (0.3x effectiveness)
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
The step penalty encourages the agent to find efficient sequences (fewer passes to reach the same improvement), matching real compiler efficiency goals.
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Grader
|
| 198 |
+
|
| 199 |
+
The grader is called at episode end and returns a score in [0.0, 1.0]:
|
| 200 |
+
|
| 201 |
+
```python
|
| 202 |
+
score = grade_by_threshold(improvement_pct, task_id)
|
| 203 |
+
score -= 0.05 * max(0, steps_used - minimum_steps_needed)
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
The efficiency deduction (0.05 per extra step) rewards agents that find the optimal chain quickly over those that brute-force all 10 steps.
|
| 207 |
+
|
| 208 |
+
---
|
| 209 |
+
|
| 210 |
+
## Baseline Scores
|
| 211 |
+
|
| 212 |
+
Measured using `gpt-4o-mini` via the OpenAI API, 5 episodes per task:
|
| 213 |
+
|
| 214 |
+
| Task | Avg Score | Avg Improvement | Best Score |
|
| 215 |
+
|---|---|---|---|
|
| 216 |
+
| Task 1 (Easy) | ~0.42 | ~28% | ~0.70 |
|
| 217 |
+
| Task 2 (Medium) | ~0.28 | ~24% | ~0.40 |
|
| 218 |
+
| Task 3 (Hard) | ~0.21 | ~21% | ~0.40 |
|
| 219 |
+
| **Overall** | **~0.30** | **~24%** | — |
|
| 220 |
+
|
| 221 |
+
The LLM agent partially discovers the alias → vectorization chain (~30% of episodes) but rarely sequences both chains correctly in Task 2/3.
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
## Setup & Usage
|
| 226 |
+
|
| 227 |
+
### Prerequisites
|
| 228 |
+
- Python 3.10+
|
| 229 |
+
- Docker (for containerized deployment)
|
| 230 |
+
|
| 231 |
+
### Local Development (no Docker)
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
# Clone / navigate to project
|
| 235 |
+
cd compiler_opt_env
|
| 236 |
+
|
| 237 |
+
# Install
|
| 238 |
+
pip install -e .
|
| 239 |
+
|
| 240 |
+
# Start server
|
| 241 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 242 |
+
|
| 243 |
+
# In another terminal — run sanity test
|
| 244 |
+
python test_env.py
|
| 245 |
+
|
| 246 |
+
# Run LLM baseline
|
| 247 |
+
export OPENAI_API_KEY=your_key
|
| 248 |
+
python baseline_agent.py --base-url http://localhost:8000 --episodes 3
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### Docker
|
| 252 |
+
|
| 253 |
+
```bash
|
| 254 |
+
# Build
|
| 255 |
+
docker build -t compiler-opt-env:latest -f server/Dockerfile .
|
| 256 |
+
|
| 257 |
+
# Run
|
| 258 |
+
docker run -p 8000:8000 compiler-opt-env:latest
|
| 259 |
+
|
| 260 |
+
# Web UI
|
| 261 |
+
open http://localhost:8000/web
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
### Programmatic Usage
|
| 265 |
+
|
| 266 |
+
```python
|
| 267 |
+
from compiler_opt_env import CompilerOptAction, CompilerOptEnv
|
| 268 |
+
from compiler_opt_env.models import TASK_EASY, TASK_MEDIUM, TASK_HARD
|
| 269 |
+
|
| 270 |
+
with CompilerOptEnv(base_url="http://localhost:8000").sync() as env:
|
| 271 |
+
# Task 1: easy
|
| 272 |
+
obs = env.reset()
|
| 273 |
+
|
| 274 |
+
# Optimal sequence for Task 1:
|
| 275 |
+
for pass_id in [13, 0, 4]: # alias → DCE → vectorization
|
| 276 |
+
result = env.step(CompilerOptAction(pass_id=pass_id, task_id=TASK_EASY))
|
| 277 |
+
print(f"{result.observation.last_pass_name}: {result.observation.improvement_pct:.1f}%")
|
| 278 |
+
|
| 279 |
+
print(f"Grader score: {result.observation.grader_score}")
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
## API Endpoints
|
| 285 |
+
|
| 286 |
+
| Endpoint | Method | Description |
|
| 287 |
+
|---|---|---|
|
| 288 |
+
| `/reset` | POST | Start new episode |
|
| 289 |
+
| `/step` | POST | Apply an action |
|
| 290 |
+
| `/state` | GET | Current episode metadata |
|
| 291 |
+
| `/schema` | GET | Action/observation schemas |
|
| 292 |
+
| `/health` | GET | Health check |
|
| 293 |
+
| `/web` | GET | Interactive web UI |
|
| 294 |
+
| `/docs` | GET | Swagger API documentation |
|
| 295 |
+
| `/ws` | WS | WebSocket for low-latency sessions |
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
## Project Structure
|
| 300 |
+
|
| 301 |
+
```
|
| 302 |
+
compiler_opt_env/
|
| 303 |
+
├── __init__.py # Exports: CompilerOptEnv, CompilerOptAction, CompilerOptObservation
|
| 304 |
+
├── models.py # Action, Observation, constants, task IDs
|
| 305 |
+
├── client.py # CompilerOptEnv(EnvClient) — HTTP/WebSocket client
|
| 306 |
+
├── baseline_agent.py # LLM baseline using OpenAI API
|
| 307 |
+
├── openenv.yaml # OpenEnv manifest
|
| 308 |
+
├── pyproject.toml # Dependencies
|
| 309 |
+
├── README.md # This file
|
| 310 |
+
└── server/
|
| 311 |
+
├── __init__.py
|
| 312 |
+
├── compiler_opt_env_environment.py # Core RL logic: passes, synergy, grader
|
| 313 |
+
├── app.py # FastAPI app
|
| 314 |
+
└── Dockerfile # Container definition
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## References
|
| 320 |
+
|
| 321 |
+
- [DeepMind MLGO: A Machine Learning Guided Compiler Optimizations Framework](https://arxiv.org/abs/2101.04808)
|
| 322 |
+
- [Meta OpenEnv](https://github.com/meta-pytorch/OpenEnv)
|
| 323 |
+
- [LLVM Optimization Passes](https://llvm.org/docs/Passes.html)
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Compiler Opt Env Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import CompilerOptEnv
|
| 10 |
+
from .models import CompilerOptAction, CompilerOptObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"CompilerOptAction",
|
| 14 |
+
"CompilerOptObservation",
|
| 15 |
+
"CompilerOptEnv",
|
| 16 |
+
]
|
baseline_agent.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Baseline inference script for the Compiler Pass Ordering RL Environment.
|
| 3 |
+
|
| 4 |
+
Runs an LLM agent (via OpenAI-compatible API) against all 3 tasks and
|
| 5 |
+
produces a reproducible baseline score report.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
export OPENAI_API_KEY=your_key_here
|
| 9 |
+
export OPENAI_BASE_URL=https://api.openai.com/v1 # optional, defaults to OpenAI
|
| 10 |
+
python baseline_agent.py --base-url http://localhost:8000
|
| 11 |
+
|
| 12 |
+
Requirements:
|
| 13 |
+
pip install openai
|
| 14 |
+
(server must be running: uvicorn server.app:app --host 0.0.0.0 --port 8000)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import time
|
| 21 |
+
|
| 22 |
+
from openai import OpenAI
|
| 23 |
+
|
| 24 |
+
from compiler_opt_env import CompilerOptAction, CompilerOptEnv
|
| 25 |
+
from compiler_opt_env.models import PASS_NAMES, TASK_EASY, TASK_MEDIUM, TASK_HARD
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Config
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 31 |
+
EPISODES = int(os.getenv("BASELINE_EPISODES", "5")) # episodes per task
|
| 32 |
+
MAX_RETRIES = 3
|
| 33 |
+
|
| 34 |
+
TASK_NAMES = {TASK_EASY: "Easy", TASK_MEDIUM: "Medium", TASK_HARD: "Hard"}
|
| 35 |
+
|
| 36 |
+
SYSTEM_PROMPT = """You are an expert compiler engineer. You are controlling a compiler
|
| 37 |
+
optimization pipeline. At each step you must choose ONE optimization pass to apply
|
| 38 |
+
to the program's Intermediate Representation (IR) to minimize its estimated runtime cost.
|
| 39 |
+
|
| 40 |
+
Available passes (use the integer ID):
|
| 41 |
+
0: dead_code_elimination — removes unreachable/unused code
|
| 42 |
+
1: constant_folding — evaluates constant expressions at compile time
|
| 43 |
+
2: loop_unrolling — expands loop bodies to reduce iteration overhead
|
| 44 |
+
3: function_inlining — replaces function calls with function body
|
| 45 |
+
4: vectorization — uses SIMD instructions for parallel computation
|
| 46 |
+
5: loop_invariant_motion — moves loop-invariant code outside the loop
|
| 47 |
+
6: strength_reduction — replaces expensive ops with cheaper equivalents
|
| 48 |
+
7: common_subexpr_elimination — eliminates redundant computations
|
| 49 |
+
8: tail_call_optimization — converts tail recursion to iteration
|
| 50 |
+
9: branch_prediction_hints — adds CPU branch prediction metadata
|
| 51 |
+
10: register_allocation — optimizes register usage
|
| 52 |
+
11: instruction_scheduling — reorders instructions to avoid pipeline stalls
|
| 53 |
+
12: memory_coalescing — combines memory accesses for cache efficiency
|
| 54 |
+
13: alias_analysis — determines which pointers can alias (enables others)
|
| 55 |
+
14: interprocedural_analysis — cross-function analysis (enables inlining)
|
| 56 |
+
|
| 57 |
+
IMPORTANT: Some passes are much more effective when specific prerequisite passes
|
| 58 |
+
have been applied first. For example, vectorization is nearly useless without
|
| 59 |
+
alias_analysis and dead_code_elimination applied first. Think carefully about
|
| 60 |
+
ordering — applying enabler passes early unlocks large gains later.
|
| 61 |
+
|
| 62 |
+
You must respond with ONLY a JSON object: {"pass_id": <integer 0-14>}
|
| 63 |
+
No explanation, no markdown, just the JSON."""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def build_user_prompt(obs) -> str:
|
| 67 |
+
applied_names = [PASS_NAMES[p] for p in obs.passes_applied]
|
| 68 |
+
available_names = {p: PASS_NAMES[p] for p in obs.passes_available}
|
| 69 |
+
|
| 70 |
+
return f"""Current program state:
|
| 71 |
+
- Program type: {obs.program_type}
|
| 72 |
+
- Estimated cost: {obs.estimated_cost:.1f} (baseline: {obs.baseline_cost:.1f})
|
| 73 |
+
- Cost reduction so far: {obs.improvement_pct:.1f}%
|
| 74 |
+
- Steps used: {obs.step_count} / {obs.max_steps}
|
| 75 |
+
- Passes applied so far (in order): {applied_names if applied_names else 'none'}
|
| 76 |
+
- Available passes: {json.dumps(available_names)}
|
| 77 |
+
- Synergy state (effectiveness multipliers): {dict(zip(obs.passes_available, [round(obs.synergy_state[p], 2) for p in obs.passes_available]))}
|
| 78 |
+
|
| 79 |
+
Task: {obs.task_description}
|
| 80 |
+
|
| 81 |
+
Which pass should be applied next? Respond with only: {{"pass_id": <integer>}}"""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def run_llm_episode(env, openai_client: OpenAI, task_id: int) -> dict:
|
| 85 |
+
"""Run one episode with the LLM agent. Returns episode result dict."""
|
| 86 |
+
result = env.reset()
|
| 87 |
+
obs = result.observation if hasattr(result, 'observation') else result
|
| 88 |
+
|
| 89 |
+
conversation = []
|
| 90 |
+
episode_rewards = []
|
| 91 |
+
|
| 92 |
+
while not obs.done:
|
| 93 |
+
user_msg = build_user_prompt(obs)
|
| 94 |
+
conversation_turn = [
|
| 95 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 96 |
+
{"role": "user", "content": user_msg},
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
# Call LLM with retries
|
| 100 |
+
pass_id = None
|
| 101 |
+
for attempt in range(MAX_RETRIES):
|
| 102 |
+
try:
|
| 103 |
+
response = openai_client.chat.completions.create(
|
| 104 |
+
model=MODEL,
|
| 105 |
+
messages=conversation_turn,
|
| 106 |
+
temperature=0.2,
|
| 107 |
+
max_tokens=50,
|
| 108 |
+
)
|
| 109 |
+
raw = response.choices[0].message.content.strip()
|
| 110 |
+
parsed = json.loads(raw)
|
| 111 |
+
pass_id = int(parsed["pass_id"])
|
| 112 |
+
if pass_id not in obs.passes_available:
|
| 113 |
+
print(f" [warn] LLM chose unavailable pass {pass_id}, picking random")
|
| 114 |
+
import random
|
| 115 |
+
pass_id = random.choice(obs.passes_available)
|
| 116 |
+
break
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f" [retry {attempt+1}] LLM parse error: {e}")
|
| 119 |
+
time.sleep(1)
|
| 120 |
+
|
| 121 |
+
if pass_id is None:
|
| 122 |
+
import random
|
| 123 |
+
pass_id = random.choice(obs.passes_available)
|
| 124 |
+
print(f" [fallback] Using random pass: {PASS_NAMES[pass_id]}")
|
| 125 |
+
|
| 126 |
+
step_result = env.step(CompilerOptAction(pass_id=pass_id, task_id=task_id))
|
| 127 |
+
obs = step_result.observation
|
| 128 |
+
episode_rewards.append(step_result.reward or 0.0)
|
| 129 |
+
|
| 130 |
+
print(f" Step {obs.step_count}: {PASS_NAMES[pass_id]:35s} "
|
| 131 |
+
f"→ improvement={obs.improvement_pct:.1f}% "
|
| 132 |
+
f"reward={step_result.reward:.4f}")
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"task_id": task_id,
|
| 136 |
+
"improvement_pct": obs.improvement_pct,
|
| 137 |
+
"grader_score": obs.grader_score,
|
| 138 |
+
"steps_used": obs.step_count,
|
| 139 |
+
"passes_applied": [PASS_NAMES[p] for p in obs.passes_applied],
|
| 140 |
+
"total_reward": sum(episode_rewards),
|
| 141 |
+
"program_type": obs.program_type,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main():
|
| 146 |
+
parser = argparse.ArgumentParser(description="Compiler Opt Env — LLM Baseline Agent")
|
| 147 |
+
parser.add_argument("--base-url", default="http://localhost:8000", help="Environment server URL")
|
| 148 |
+
parser.add_argument("--episodes", type=int, default=EPISODES, help="Episodes per task")
|
| 149 |
+
parser.add_argument("--model", default=MODEL, help="OpenAI model name")
|
| 150 |
+
args = parser.parse_args()
|
| 151 |
+
|
| 152 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 153 |
+
if not api_key:
|
| 154 |
+
raise ValueError("OPENAI_API_KEY environment variable not set")
|
| 155 |
+
|
| 156 |
+
openai_client = OpenAI(
|
| 157 |
+
api_key=api_key,
|
| 158 |
+
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
print(f"\n{'='*65}")
|
| 162 |
+
print(f" Compiler Pass Ordering — LLM Baseline ({args.model})")
|
| 163 |
+
print(f" Server: {args.base_url} | Episodes per task: {args.episodes}")
|
| 164 |
+
print(f"{'='*65}\n")
|
| 165 |
+
|
| 166 |
+
all_results = []
|
| 167 |
+
|
| 168 |
+
with CompilerOptEnv(base_url=args.base_url).sync() as env:
|
| 169 |
+
for task_id in [TASK_EASY, TASK_MEDIUM, TASK_HARD]:
|
| 170 |
+
print(f"\n--- Task {task_id} ({TASK_NAMES[task_id]}) ---")
|
| 171 |
+
task_results = []
|
| 172 |
+
|
| 173 |
+
for ep in range(args.episodes):
|
| 174 |
+
print(f" Episode {ep+1}/{args.episodes}:")
|
| 175 |
+
result = run_llm_episode(env, openai_client, task_id)
|
| 176 |
+
task_results.append(result)
|
| 177 |
+
print(f" → Grader score: {result['grader_score']:.3f} "
|
| 178 |
+
f"Improvement: {result['improvement_pct']:.1f}%\n")
|
| 179 |
+
|
| 180 |
+
avg_score = sum(r['grader_score'] or 0 for r in task_results) / len(task_results)
|
| 181 |
+
avg_improv = sum(r['improvement_pct'] for r in task_results) / len(task_results)
|
| 182 |
+
all_results.extend(task_results)
|
| 183 |
+
|
| 184 |
+
print(f" Task {task_id} average — score: {avg_score:.3f} improvement: {avg_improv:.1f}%")
|
| 185 |
+
|
| 186 |
+
# ---------------------------------------------------------------------------
|
| 187 |
+
# Summary report
|
| 188 |
+
# ---------------------------------------------------------------------------
|
| 189 |
+
print(f"\n{'='*65}")
|
| 190 |
+
print(" BASELINE SCORE REPORT")
|
| 191 |
+
print(f"{'='*65}")
|
| 192 |
+
print(f" Model: {args.model}")
|
| 193 |
+
print(f" Episodes per task: {args.episodes}\n")
|
| 194 |
+
|
| 195 |
+
for task_id in [TASK_EASY, TASK_MEDIUM, TASK_HARD]:
|
| 196 |
+
task_r = [r for r in all_results if r['task_id'] == task_id]
|
| 197 |
+
scores = [r['grader_score'] or 0 for r in task_r]
|
| 198 |
+
improvs = [r['improvement_pct'] for r in task_r]
|
| 199 |
+
print(f" Task {task_id} ({TASK_NAMES[task_id]:6s}): "
|
| 200 |
+
f"avg_score={sum(scores)/len(scores):.3f} "
|
| 201 |
+
f"avg_improvement={sum(improvs)/len(improvs):.1f}% "
|
| 202 |
+
f"best={max(scores):.3f}")
|
| 203 |
+
|
| 204 |
+
overall = sum(r['grader_score'] or 0 for r in all_results) / len(all_results)
|
| 205 |
+
print(f"\n Overall average score: {overall:.3f} / 1.000")
|
| 206 |
+
print(f"{'='*65}\n")
|
| 207 |
+
|
| 208 |
+
# Save results to JSON
|
| 209 |
+
output_path = "baseline_results.json"
|
| 210 |
+
with open(output_path, "w") as f:
|
| 211 |
+
json.dump({
|
| 212 |
+
"model": args.model,
|
| 213 |
+
"episodes": args.episodes,
|
| 214 |
+
"results": all_results,
|
| 215 |
+
"summary": {
|
| 216 |
+
"overall_avg_score": overall,
|
| 217 |
+
"by_task": {
|
| 218 |
+
str(tid): {
|
| 219 |
+
"avg_score": sum(r['grader_score'] or 0 for r in all_results if r['task_id'] == tid)
|
| 220 |
+
/ sum(1 for r in all_results if r['task_id'] == tid),
|
| 221 |
+
"avg_improvement_pct": sum(r['improvement_pct'] for r in all_results if r['task_id'] == tid)
|
| 222 |
+
/ sum(1 for r in all_results if r['task_id'] == tid),
|
| 223 |
+
}
|
| 224 |
+
for tid in [TASK_EASY, TASK_MEDIUM, TASK_HARD]
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}, f, indent=2)
|
| 228 |
+
|
| 229 |
+
print(f"Full results saved to: {output_path}")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == "__main__":
|
| 233 |
+
main()
|
client.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Compiler Pass Ordering Environment Client."""
|
| 8 |
+
|
| 9 |
+
from typing import Dict
|
| 10 |
+
|
| 11 |
+
from openenv.core import EnvClient
|
| 12 |
+
from openenv.core.client_types import StepResult
|
| 13 |
+
from openenv.core.env_server.types import State
|
| 14 |
+
|
| 15 |
+
from .models import CompilerOptAction, CompilerOptObservation
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class CompilerOptEnv(
|
| 19 |
+
EnvClient[CompilerOptAction, CompilerOptObservation, State]
|
| 20 |
+
):
|
| 21 |
+
"""
|
| 22 |
+
Client for the Compiler Pass Ordering Environment.
|
| 23 |
+
|
| 24 |
+
Maintains a persistent WebSocket connection to the environment server.
|
| 25 |
+
Each client instance has its own dedicated environment session.
|
| 26 |
+
|
| 27 |
+
Example (sync):
|
| 28 |
+
>>> with CompilerOptEnv(base_url="http://localhost:8000").sync() as env:
|
| 29 |
+
... obs = env.reset()
|
| 30 |
+
... result = env.step(CompilerOptAction(pass_id=13, task_id=1))
|
| 31 |
+
... print(result.observation.improvement_pct)
|
| 32 |
+
|
| 33 |
+
Example (async):
|
| 34 |
+
>>> async with CompilerOptEnv(base_url="http://localhost:8000") as env:
|
| 35 |
+
... obs = await env.reset()
|
| 36 |
+
... result = await env.step(CompilerOptAction(pass_id=13, task_id=1))
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def _step_payload(self, action: CompilerOptAction) -> Dict:
|
| 40 |
+
return {
|
| 41 |
+
"pass_id": action.pass_id,
|
| 42 |
+
"task_id": action.task_id,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
def _parse_result(self, payload: Dict) -> StepResult[CompilerOptObservation]:
|
| 46 |
+
obs_data = payload.get("observation", {})
|
| 47 |
+
observation = CompilerOptObservation(
|
| 48 |
+
estimated_cost = obs_data.get("estimated_cost", 0.0),
|
| 49 |
+
baseline_cost = obs_data.get("baseline_cost", 0.0),
|
| 50 |
+
num_instructions = obs_data.get("num_instructions", 0),
|
| 51 |
+
num_loops = obs_data.get("num_loops", 0),
|
| 52 |
+
num_branches = obs_data.get("num_branches", 0),
|
| 53 |
+
num_functions = obs_data.get("num_functions", 0),
|
| 54 |
+
loop_depth = obs_data.get("loop_depth", 0),
|
| 55 |
+
program_type = obs_data.get("program_type", ""),
|
| 56 |
+
passes_applied = obs_data.get("passes_applied", []),
|
| 57 |
+
passes_available = obs_data.get("passes_available", []),
|
| 58 |
+
step_count = obs_data.get("step_count", 0),
|
| 59 |
+
max_steps = obs_data.get("max_steps", 10),
|
| 60 |
+
synergy_state = obs_data.get("synergy_state", [1.0] * 15),
|
| 61 |
+
task_id = obs_data.get("task_id", 3),
|
| 62 |
+
task_description = obs_data.get("task_description", ""),
|
| 63 |
+
done = payload.get("done", False),
|
| 64 |
+
reward = payload.get("reward", 0.0),
|
| 65 |
+
improvement_pct = obs_data.get("improvement_pct", 0.0),
|
| 66 |
+
last_pass_name = obs_data.get("last_pass_name"),
|
| 67 |
+
grader_score = obs_data.get("grader_score"),
|
| 68 |
+
)
|
| 69 |
+
return StepResult(
|
| 70 |
+
observation = observation,
|
| 71 |
+
reward = payload.get("reward"),
|
| 72 |
+
done = payload.get("done", False),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 76 |
+
return State(
|
| 77 |
+
episode_id = payload.get("episode_id"),
|
| 78 |
+
step_count = payload.get("step_count", 0),
|
| 79 |
+
)
|
models.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Data models for the Compiler Pass Ordering RL Environment.
|
| 9 |
+
|
| 10 |
+
This environment simulates compiler optimization — a real task performed by
|
| 11 |
+
compilers like GCC and LLVM. An agent must select a sequence of optimization
|
| 12 |
+
passes to apply to a program's Intermediate Representation (IR) to minimize
|
| 13 |
+
estimated runtime cost.
|
| 14 |
+
|
| 15 |
+
Three tasks of increasing difficulty:
|
| 16 |
+
Task 1 (easy): Single-chain unlock. One prerequisite pass unlocks one target pass.
|
| 17 |
+
Task 2 (medium): Two-chain unlock. Agent must discover two independent synergy chains.
|
| 18 |
+
Task 3 (hard): Full optimization. Agent must sequence all passes optimally across
|
| 19 |
+
a complex program with many interacting synergy gates.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from typing import List, Optional
|
| 23 |
+
from openenv.core.env_server.types import Action, Observation
|
| 24 |
+
from pydantic import Field
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Pass registry
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
PASS_NAMES = {
|
| 31 |
+
0: "dead_code_elimination",
|
| 32 |
+
1: "constant_folding",
|
| 33 |
+
2: "loop_unrolling",
|
| 34 |
+
3: "function_inlining",
|
| 35 |
+
4: "vectorization",
|
| 36 |
+
5: "loop_invariant_motion",
|
| 37 |
+
6: "strength_reduction",
|
| 38 |
+
7: "common_subexpr_elimination",
|
| 39 |
+
8: "tail_call_optimization",
|
| 40 |
+
9: "branch_prediction_hints",
|
| 41 |
+
10: "register_allocation",
|
| 42 |
+
11: "instruction_scheduling",
|
| 43 |
+
12: "memory_coalescing",
|
| 44 |
+
13: "alias_analysis",
|
| 45 |
+
14: "interprocedural_analysis",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
NUM_PASSES = len(PASS_NAMES)
|
| 49 |
+
MAX_STEPS = 10
|
| 50 |
+
|
| 51 |
+
# Task IDs
|
| 52 |
+
TASK_EASY = 1
|
| 53 |
+
TASK_MEDIUM = 2
|
| 54 |
+
TASK_HARD = 3
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Action
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
class CompilerOptAction(Action):
|
| 61 |
+
"""
|
| 62 |
+
Select which optimization pass to apply next.
|
| 63 |
+
|
| 64 |
+
pass_id: integer in [0, 14]. See PASS_NAMES for the full mapping.
|
| 65 |
+
Applying a pass that has already been applied this episode incurs a penalty.
|
| 66 |
+
Applying a pass whose prerequisites have not been met applies it at reduced
|
| 67 |
+
effectiveness (0.3x) — the agent must discover correct ordering.
|
| 68 |
+
"""
|
| 69 |
+
pass_id: int = Field(..., ge=0, le=14, description="ID of the optimization pass to apply (0–14)")
|
| 70 |
+
task_id: int = Field(default=TASK_HARD, ge=1, le=3, description="Task difficulty: 1=easy, 2=medium, 3=hard")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
# Observation
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
class CompilerOptObservation(Observation):
|
| 77 |
+
"""
|
| 78 |
+
Full observable state of the simulated compiler IR after each step.
|
| 79 |
+
|
| 80 |
+
The agent uses this to decide which pass to apply next. Key signals:
|
| 81 |
+
- estimated_cost / baseline_cost: how much optimization has been achieved
|
| 82 |
+
- passes_applied: history of applied passes (order matters for synergy)
|
| 83 |
+
- synergy_state: current effectiveness multiplier for each pass
|
| 84 |
+
- passes_available: which passes have not yet been applied
|
| 85 |
+
- improvement_pct: total % cost reduction from baseline so far
|
| 86 |
+
"""
|
| 87 |
+
# Cost tracking
|
| 88 |
+
estimated_cost: float = Field(default=0.0, description="Current estimated runtime cost")
|
| 89 |
+
baseline_cost: float = Field(default=0.0, description="Cost before any optimization")
|
| 90 |
+
|
| 91 |
+
# IR structural features (static for the episode, describe program type)
|
| 92 |
+
num_instructions: int = Field(default=0, description="Total instruction count in the IR")
|
| 93 |
+
num_loops: int = Field(default=0, description="Number of loop structures")
|
| 94 |
+
num_branches: int = Field(default=0, description="Number of branch instructions")
|
| 95 |
+
num_functions: int = Field(default=0, description="Number of functions")
|
| 96 |
+
loop_depth: int = Field(default=0, description="Maximum loop nesting depth")
|
| 97 |
+
program_type: str = Field(default="", description="Human-readable program category")
|
| 98 |
+
|
| 99 |
+
# Episode progress
|
| 100 |
+
passes_applied: List[int] = Field(default_factory=list, description="Ordered list of pass IDs applied so far")
|
| 101 |
+
passes_available: List[int] = Field(default_factory=list, description="Pass IDs not yet applied this episode")
|
| 102 |
+
step_count: int = Field(default=0, description="Number of steps taken this episode")
|
| 103 |
+
max_steps: int = Field(default=MAX_STEPS, description="Maximum steps allowed per episode")
|
| 104 |
+
|
| 105 |
+
# Synergy state: current effectiveness multiplier for each pass given history
|
| 106 |
+
synergy_state: List[float] = Field(
|
| 107 |
+
default_factory=lambda: [1.0] * NUM_PASSES,
|
| 108 |
+
description="Per-pass effectiveness multiplier. >1 = boosted by prior passes, <1 = suppressed."
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Task info
|
| 112 |
+
task_id: int = Field(default=TASK_HARD, description="Current task difficulty (1/2/3)")
|
| 113 |
+
task_description: str = Field(default="", description="Human-readable task goal")
|
| 114 |
+
|
| 115 |
+
# Terminal / result fields
|
| 116 |
+
done: bool = Field(default=False, description="Whether this episode has ended")
|
| 117 |
+
reward: float = Field(default=0.0, description="Reward received for the last action")
|
| 118 |
+
improvement_pct: float = Field(default=0.0, description="Total % cost reduction from baseline")
|
| 119 |
+
last_pass_name: Optional[str] = Field(default=None, description="Name of the last pass applied")
|
| 120 |
+
|
| 121 |
+
# Grader score (populated on done=True)
|
| 122 |
+
grader_score: Optional[float] = Field(
|
| 123 |
+
default=None,
|
| 124 |
+
description="Final task score 0.0–1.0, populated when done=True"
|
| 125 |
+
)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: compiler_opt_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
|
pyproject.toml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-compiler_opt_env"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Compiler Pass Ordering RL Environment for OpenEnv"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
"openenv-core[core]>=0.2.2",
|
| 18 |
+
"numpy>=1.24.0",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[project.optional-dependencies]
|
| 22 |
+
dev = [
|
| 23 |
+
"pytest>=8.0.0",
|
| 24 |
+
"pytest-cov>=4.0.0",
|
| 25 |
+
"openai>=1.0.0",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
[project.scripts]
|
| 29 |
+
server = "compiler_opt_env.server.app:main"
|
| 30 |
+
|
| 31 |
+
[tool.setuptools]
|
| 32 |
+
include-package-data = true
|
| 33 |
+
packages = ["compiler_opt_env", "compiler_opt_env.server"]
|
| 34 |
+
package-dir = { "compiler_opt_env" = ".", "compiler_opt_env.server" = "server" }
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Compiler Opt Env environment server components."""
|
| 8 |
+
|
| 9 |
+
from .compiler_opt_env_environment import CompilerOptEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["CompilerOptEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""FastAPI application for the Compiler Pass Ordering Environment."""
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from openenv.core.env_server.http_server import create_app
|
| 11 |
+
except Exception as e:
|
| 12 |
+
raise ImportError(
|
| 13 |
+
"openenv is required. Install with: pip install -e ."
|
| 14 |
+
) from e
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from ..models import CompilerOptAction, CompilerOptObservation
|
| 18 |
+
from .compiler_opt_env_environment import CompilerOptEnvironment
|
| 19 |
+
except (ImportError, ModuleNotFoundError):
|
| 20 |
+
from models import CompilerOptAction, CompilerOptObservation
|
| 21 |
+
from server.compiler_opt_env_environment import CompilerOptEnvironment
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
app = create_app(
|
| 25 |
+
CompilerOptEnvironment,
|
| 26 |
+
CompilerOptAction,
|
| 27 |
+
CompilerOptObservation,
|
| 28 |
+
env_name="compiler_opt_env",
|
| 29 |
+
max_concurrent_envs=1,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 34 |
+
import uvicorn
|
| 35 |
+
uvicorn.run(app, host=host, port=port)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__":
|
| 39 |
+
import argparse
|
| 40 |
+
parser = argparse.ArgumentParser()
|
| 41 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
+
main(port=args.port)
|
server/compiler_opt_env_environment.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Compiler Pass Ordering RL Environment — Core Logic.
|
| 9 |
+
|
| 10 |
+
Simulates the compiler optimization problem: given a program's Intermediate
|
| 11 |
+
Representation (IR), select a sequence of optimization passes to minimize
|
| 12 |
+
estimated runtime cost. This is a real problem solved by GCC/LLVM today using
|
| 13 |
+
hand-tuned heuristics (-O2, -O3). RL finds better orderings.
|
| 14 |
+
|
| 15 |
+
Why RL is necessary (not greedy):
|
| 16 |
+
Passes interact via prerequisite gates. A pass applied without its prerequisites
|
| 17 |
+
fires at 0.3x effectiveness. With all prerequisites met, it fires at 5–8x.
|
| 18 |
+
A greedy agent never applies low-value enabler passes (alias_analysis = 0.01
|
| 19 |
+
base effect), so it never unlocks the high-value chains. RL learns to sacrifice
|
| 20 |
+
early reward to unlock these chains — exactly the credit assignment problem RL
|
| 21 |
+
is designed for.
|
| 22 |
+
|
| 23 |
+
Three tasks:
|
| 24 |
+
Task 1 (easy): One synergy chain. alias_analysis → vectorization.
|
| 25 |
+
Grader threshold: >25% cost reduction.
|
| 26 |
+
Task 2 (medium): Two independent chains. Agent must find both.
|
| 27 |
+
Grader threshold: >35% cost reduction.
|
| 28 |
+
Task 3 (hard): Full program. Multiple chains, suppressions, program diversity.
|
| 29 |
+
Grader threshold: >42% cost reduction.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import random
|
| 33 |
+
from uuid import uuid4
|
| 34 |
+
|
| 35 |
+
import numpy as np
|
| 36 |
+
|
| 37 |
+
from openenv.core.env_server.interfaces import Environment
|
| 38 |
+
from openenv.core.env_server.types import State
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from ..models import (
|
| 42 |
+
CompilerOptAction, CompilerOptObservation,
|
| 43 |
+
PASS_NAMES, NUM_PASSES, MAX_STEPS,
|
| 44 |
+
TASK_EASY, TASK_MEDIUM, TASK_HARD,
|
| 45 |
+
)
|
| 46 |
+
except ImportError:
|
| 47 |
+
from models import (
|
| 48 |
+
CompilerOptAction, CompilerOptObservation,
|
| 49 |
+
PASS_NAMES, NUM_PASSES, MAX_STEPS,
|
| 50 |
+
TASK_EASY, TASK_MEDIUM, TASK_HARD,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# Pass base effects: fractional cost reduction on an "average" program.
|
| 56 |
+
# Enabler passes (alias_analysis, interprocedural) have very low base effects —
|
| 57 |
+
# their value comes entirely from unlocking other passes.
|
| 58 |
+
# ---------------------------------------------------------------------------
|
| 59 |
+
BASE_PASS_EFFECTS = np.array([
|
| 60 |
+
0.08, # 0: dead_code_elimination
|
| 61 |
+
0.06, # 1: constant_folding
|
| 62 |
+
0.10, # 2: loop_unrolling
|
| 63 |
+
0.03, # 3: function_inlining (weak alone, needs interprocedural)
|
| 64 |
+
0.02, # 4: vectorization (very weak alone, needs alias+DCE)
|
| 65 |
+
0.06, # 5: loop_invariant_motion
|
| 66 |
+
0.05, # 6: strength_reduction
|
| 67 |
+
0.07, # 7: common_subexpr_elimination
|
| 68 |
+
0.04, # 8: tail_call_optimization
|
| 69 |
+
0.03, # 9: branch_prediction_hints
|
| 70 |
+
0.05, # 10: register_allocation
|
| 71 |
+
0.04, # 11: instruction_scheduling
|
| 72 |
+
0.06, # 12: memory_coalescing (weak alone, needs alias)
|
| 73 |
+
0.01, # 13: alias_analysis (sacrificial enabler — very low base)
|
| 74 |
+
0.01, # 14: interprocedural_analysis (sacrificial enabler — very low base)
|
| 75 |
+
])
|
| 76 |
+
|
| 77 |
+
# ---------------------------------------------------------------------------
|
| 78 |
+
# Prerequisite gates: a pass fires at full synergy ONLY if ALL listed
|
| 79 |
+
# prerequisite passes have already been applied. Otherwise 0.3x (suppressed).
|
| 80 |
+
#
|
| 81 |
+
# These are the "unlock chains" the RL agent must discover:
|
| 82 |
+
# alias(13) + DCE(0) → vectorization(4): 7.0x [key chain 1]
|
| 83 |
+
# interprocedural(14) → function_inlining(3): 5.0x [key chain 2]
|
| 84 |
+
# alias(13) → memory_coalescing(12): 3.5x [secondary chain]
|
| 85 |
+
# alias(13) + const_fold(1) → strength_red(6): 3.0x [secondary chain]
|
| 86 |
+
# reg_alloc(10) → instr_scheduling(11): 2.5x
|
| 87 |
+
# LIM(5) → loop_unrolling(2): 2.0x
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
PREREQ_GATES = {
|
| 90 |
+
4: ([13, 0], 7.0), # vectorization: alias_analysis + DCE required
|
| 91 |
+
3: ([14], 5.0), # function_inlining: interprocedural required
|
| 92 |
+
12: ([13], 3.5), # memory_coalescing: alias_analysis required
|
| 93 |
+
6: ([13, 1], 3.0), # strength_reduction: alias + constant_folding required
|
| 94 |
+
11: ([10], 2.5), # instruction_scheduling: register_allocation required
|
| 95 |
+
2: ([5], 2.0), # loop_unrolling: loop_invariant_motion required
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
# Suppression matrix: applying pass i suppresses future effectiveness of pass j.
|
| 99 |
+
# This penalizes greedy's natural sequence (it picks high-base passes early,
|
| 100 |
+
# which suppresses other passes it would have picked later).
|
| 101 |
+
PASS_SYNERGY = np.ones((NUM_PASSES, NUM_PASSES), dtype=float)
|
| 102 |
+
PASS_SYNERGY[2, 5] = 0.3 # early loop_unrolling suppresses LIM
|
| 103 |
+
PASS_SYNERGY[3, 0] = 0.3 # early inlining (without interproc) suppresses DCE
|
| 104 |
+
PASS_SYNERGY[3, 7] = 0.3 # early inlining suppresses CSE
|
| 105 |
+
|
| 106 |
+
# ---------------------------------------------------------------------------
|
| 107 |
+
# Program templates: different programs = different optimal orderings.
|
| 108 |
+
# This prevents the agent from memorizing a single fixed sequence.
|
| 109 |
+
# Fields: (name, instructions, loops, branches, functions, loop_depth, cost_scale)
|
| 110 |
+
# ---------------------------------------------------------------------------
|
| 111 |
+
ALL_PROGRAMS = [
|
| 112 |
+
("loop_heavy", 500, 20, 10, 5, 4, 1.8),
|
| 113 |
+
("branch_heavy", 400, 5, 40, 8, 2, 1.4),
|
| 114 |
+
("compute_heavy", 800, 15, 15, 3, 3, 2.0),
|
| 115 |
+
("small_utility", 150, 3, 8, 12, 1, 1.1),
|
| 116 |
+
("recursive", 300, 8, 20, 6, 6, 1.6),
|
| 117 |
+
("vectorizable", 600, 25, 5, 4, 3, 1.9),
|
| 118 |
+
("inlining_heavy", 450, 10, 12, 20, 2, 1.5),
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
# Task 1: only vectorizable programs (single chain to discover)
|
| 122 |
+
TASK1_PROGRAMS = [p for p in ALL_PROGRAMS if p[0] in ("vectorizable", "loop_heavy")]
|
| 123 |
+
# Task 2: programs where both key chains matter
|
| 124 |
+
TASK2_PROGRAMS = [p for p in ALL_PROGRAMS if p[0] in ("compute_heavy", "inlining_heavy", "recursive")]
|
| 125 |
+
# Task 3: all programs
|
| 126 |
+
TASK3_PROGRAMS = ALL_PROGRAMS
|
| 127 |
+
|
| 128 |
+
TASK_DESCRIPTIONS = {
|
| 129 |
+
TASK_EASY: "Apply passes to a vectorizable program. Discover the alias_analysis → vectorization unlock chain. Target: >25% cost reduction.",
|
| 130 |
+
TASK_MEDIUM: "Apply passes to a compute/inlining-heavy program. Discover two independent synergy chains. Target: >35% cost reduction.",
|
| 131 |
+
TASK_HARD: "Apply passes to any program type. Discover and sequence all synergy chains optimally. Target: >42% cost reduction.",
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# Per-program modifiers: how effective each pass is on each program type
|
| 135 |
+
PROGRAM_MODIFIERS = {
|
| 136 |
+
"loop_heavy": {2: 1.5, 5: 1.4, 4: 1.3},
|
| 137 |
+
"branch_heavy": {9: 1.5, 0: 1.3, 6: 1.2},
|
| 138 |
+
"compute_heavy": {4: 1.6, 12: 1.4, 10: 1.3},
|
| 139 |
+
"small_utility": {1: 1.4, 7: 1.3, 8: 1.2},
|
| 140 |
+
"recursive": {3: 1.5, 14: 1.4, 8: 1.3},
|
| 141 |
+
"vectorizable": {4: 1.8, 13: 1.5, 12: 1.4},
|
| 142 |
+
"inlining_heavy": {3: 1.6, 14: 1.5, 0: 1.2},
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Grader thresholds: minimum improvement% to score above 0
|
| 146 |
+
GRADER_THRESHOLDS = {
|
| 147 |
+
TASK_EASY: {"excellent": 0.35, "good": 0.25, "pass": 0.15},
|
| 148 |
+
TASK_MEDIUM: {"excellent": 0.45, "good": 0.35, "pass": 0.22},
|
| 149 |
+
TASK_HARD: {"excellent": 0.52, "good": 0.42, "pass": 0.28},
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class CompilerOptEnvironment(Environment):
|
| 154 |
+
"""
|
| 155 |
+
Compiler pass ordering RL environment implementing the full OpenEnv interface.
|
| 156 |
+
|
| 157 |
+
Each episode:
|
| 158 |
+
1. A program template is sampled (based on task difficulty)
|
| 159 |
+
2. The agent applies up to MAX_STEPS passes from 15 available passes
|
| 160 |
+
3. Per-step reward = marginal improvement - step penalty
|
| 161 |
+
4. Terminal grader scores performance 0.0–1.0
|
| 162 |
+
|
| 163 |
+
Greedy baseline achieves ~19-24% (picks high-base passes, misses unlock chains).
|
| 164 |
+
Trained RL agent achieves ~42-50% by learning prerequisite sequences.
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 168 |
+
|
| 169 |
+
def __init__(self):
|
| 170 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 171 |
+
self._program = None
|
| 172 |
+
self._task_id = TASK_HARD
|
| 173 |
+
self._baseline_cost = 0.0
|
| 174 |
+
self._current_cost = 0.0
|
| 175 |
+
self._passes_applied: list[int] = []
|
| 176 |
+
self._synergy_state = np.ones(NUM_PASSES)
|
| 177 |
+
|
| 178 |
+
# ------------------------------------------------------------------
|
| 179 |
+
# OpenEnv interface
|
| 180 |
+
# ------------------------------------------------------------------
|
| 181 |
+
|
| 182 |
+
def reset(self, task_id: int = TASK_HARD) -> CompilerOptObservation:
|
| 183 |
+
"""
|
| 184 |
+
Start a new episode.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
task_id: 1=easy, 2=medium, 3=hard. Defaults to hard (full task).
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Initial CompilerOptObservation with baseline cost and program features.
|
| 191 |
+
"""
|
| 192 |
+
self._task_id = task_id
|
| 193 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 194 |
+
self._passes_applied = []
|
| 195 |
+
self._synergy_state = np.ones(NUM_PASSES)
|
| 196 |
+
|
| 197 |
+
programs = {
|
| 198 |
+
TASK_EASY: TASK1_PROGRAMS,
|
| 199 |
+
TASK_MEDIUM: TASK2_PROGRAMS,
|
| 200 |
+
TASK_HARD: TASK3_PROGRAMS,
|
| 201 |
+
}.get(task_id, TASK3_PROGRAMS)
|
| 202 |
+
|
| 203 |
+
self._program = random.choice(programs)
|
| 204 |
+
_, instructions, loops, branches, functions, loop_depth, cost_scale = self._program
|
| 205 |
+
|
| 206 |
+
noise = random.uniform(0.9, 1.1)
|
| 207 |
+
self._baseline_cost = 1000.0 * cost_scale * noise
|
| 208 |
+
self._current_cost = self._baseline_cost
|
| 209 |
+
|
| 210 |
+
return self._make_observation(reward=0.0, done=False, last_pass_id=None)
|
| 211 |
+
|
| 212 |
+
def step(self, action: CompilerOptAction) -> CompilerOptObservation:
|
| 213 |
+
"""
|
| 214 |
+
Apply an optimization pass.
|
| 215 |
+
|
| 216 |
+
If the pass has been applied before: penalty reward, no cost change.
|
| 217 |
+
If prerequisites not met: pass fires at 0.3x (reduced effectiveness).
|
| 218 |
+
If prerequisites met: pass fires at full synergy multiplier.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
CompilerOptObservation with updated cost, synergy state, and reward.
|
| 222 |
+
"""
|
| 223 |
+
self._state.step_count += 1
|
| 224 |
+
|
| 225 |
+
# Use task_id from action if provided, otherwise keep current
|
| 226 |
+
task_id = getattr(action, 'task_id', self._task_id)
|
| 227 |
+
pass_id = action.pass_id
|
| 228 |
+
|
| 229 |
+
# Guard: reset was never called
|
| 230 |
+
if self._program is None:
|
| 231 |
+
self.reset(task_id)
|
| 232 |
+
|
| 233 |
+
# Penalize re-application
|
| 234 |
+
if pass_id in self._passes_applied:
|
| 235 |
+
done = self._state.step_count >= MAX_STEPS
|
| 236 |
+
return self._make_observation(reward=-0.3, done=done, last_pass_id=pass_id)
|
| 237 |
+
|
| 238 |
+
# Compute effectiveness
|
| 239 |
+
base_effect = BASE_PASS_EFFECTS[pass_id]
|
| 240 |
+
synergy_mult = self._compute_gated_synergy(pass_id)
|
| 241 |
+
program_mod = self._program_modifier(pass_id)
|
| 242 |
+
|
| 243 |
+
cost_reduction = base_effect * synergy_mult * program_mod * self._current_cost
|
| 244 |
+
cost_reduction = max(0.0, cost_reduction)
|
| 245 |
+
|
| 246 |
+
prev_cost = self._current_cost
|
| 247 |
+
self._current_cost = max(0.0, self._current_cost - cost_reduction)
|
| 248 |
+
|
| 249 |
+
# Update suppression state for future passes
|
| 250 |
+
for future_pass in range(NUM_PASSES):
|
| 251 |
+
self._synergy_state[future_pass] *= PASS_SYNERGY[pass_id][future_pass]
|
| 252 |
+
|
| 253 |
+
self._passes_applied.append(pass_id)
|
| 254 |
+
|
| 255 |
+
# Per-step shaped reward
|
| 256 |
+
marginal_improvement = (prev_cost - self._current_cost) / self._baseline_cost
|
| 257 |
+
step_penalty = 0.02
|
| 258 |
+
reward = marginal_improvement - step_penalty
|
| 259 |
+
|
| 260 |
+
total_improvement = self._total_improvement()
|
| 261 |
+
done = (self._state.step_count >= MAX_STEPS) or (len(self._passes_applied) >= NUM_PASSES)
|
| 262 |
+
|
| 263 |
+
if done:
|
| 264 |
+
reward += self._terminal_bonus(total_improvement)
|
| 265 |
+
|
| 266 |
+
return self._make_observation(reward=reward, done=done, last_pass_id=pass_id)
|
| 267 |
+
|
| 268 |
+
@property
|
| 269 |
+
def state(self) -> State:
|
| 270 |
+
"""Return current episode state (episode_id, step_count)."""
|
| 271 |
+
return self._state
|
| 272 |
+
|
| 273 |
+
# ------------------------------------------------------------------
|
| 274 |
+
# Grader: called externally to score a completed episode
|
| 275 |
+
# ------------------------------------------------------------------
|
| 276 |
+
|
| 277 |
+
def grade(self) -> float:
|
| 278 |
+
"""
|
| 279 |
+
Score the agent's performance on this episode. Returns 0.0–1.0.
|
| 280 |
+
|
| 281 |
+
Scoring rubric (per task):
|
| 282 |
+
score = 1.0 if improvement >= excellent threshold
|
| 283 |
+
score = 0.7 if improvement >= good threshold
|
| 284 |
+
score = 0.4 if improvement >= pass threshold
|
| 285 |
+
score = 0.0 if improvement < pass threshold (or made things worse)
|
| 286 |
+
|
| 287 |
+
Additionally deducts 0.05 per step beyond the minimum needed
|
| 288 |
+
(encourages efficient sequencing, not just throwing passes at the wall).
|
| 289 |
+
"""
|
| 290 |
+
improvement = self._total_improvement()
|
| 291 |
+
thresholds = GRADER_THRESHOLDS[self._task_id]
|
| 292 |
+
|
| 293 |
+
if improvement >= thresholds["excellent"]:
|
| 294 |
+
base_score = 1.0
|
| 295 |
+
elif improvement >= thresholds["good"]:
|
| 296 |
+
base_score = 0.7
|
| 297 |
+
elif improvement >= thresholds["pass"]:
|
| 298 |
+
base_score = 0.4
|
| 299 |
+
else:
|
| 300 |
+
return 0.0
|
| 301 |
+
|
| 302 |
+
# Efficiency deduction: penalize using more steps than necessary
|
| 303 |
+
steps_used = len(self._passes_applied)
|
| 304 |
+
min_steps = self._minimum_steps_needed()
|
| 305 |
+
extra_steps = max(0, steps_used - min_steps)
|
| 306 |
+
efficiency_penalty = extra_steps * 0.05
|
| 307 |
+
|
| 308 |
+
return max(0.0, round(base_score - efficiency_penalty, 3))
|
| 309 |
+
|
| 310 |
+
# ------------------------------------------------------------------
|
| 311 |
+
# Internal helpers
|
| 312 |
+
# ------------------------------------------------------------------
|
| 313 |
+
|
| 314 |
+
def _compute_gated_synergy(self, pass_id: int) -> float:
|
| 315 |
+
"""
|
| 316 |
+
Return the synergy multiplier for pass_id given current pass history.
|
| 317 |
+
|
| 318 |
+
If the pass has defined prerequisite gates and ALL prerequisites have
|
| 319 |
+
been applied: returns the full synergy multiplier.
|
| 320 |
+
If prerequisites are NOT all met: returns 0.3 (strong suppression).
|
| 321 |
+
If no prerequisites defined: returns 1.0 (base effect only).
|
| 322 |
+
"""
|
| 323 |
+
if pass_id not in PREREQ_GATES:
|
| 324 |
+
return 1.0
|
| 325 |
+
|
| 326 |
+
prereqs, multiplier = PREREQ_GATES[pass_id]
|
| 327 |
+
applied = set(self._passes_applied)
|
| 328 |
+
|
| 329 |
+
if all(p in applied for p in prereqs):
|
| 330 |
+
return multiplier
|
| 331 |
+
else:
|
| 332 |
+
return 0.3 # prerequisites not met — reduced effectiveness
|
| 333 |
+
|
| 334 |
+
def _program_modifier(self, pass_id: int) -> float:
|
| 335 |
+
"""Per-program-type effectiveness modifier for the given pass."""
|
| 336 |
+
if self._program is None:
|
| 337 |
+
return 1.0
|
| 338 |
+
program_name = self._program[0]
|
| 339 |
+
return PROGRAM_MODIFIERS.get(program_name, {}).get(pass_id, 1.0)
|
| 340 |
+
|
| 341 |
+
def _total_improvement(self) -> float:
|
| 342 |
+
"""Fraction of baseline cost eliminated so far."""
|
| 343 |
+
if self._baseline_cost <= 0:
|
| 344 |
+
return 0.0
|
| 345 |
+
return (self._baseline_cost - self._current_cost) / self._baseline_cost
|
| 346 |
+
|
| 347 |
+
def _minimum_steps_needed(self) -> int:
|
| 348 |
+
"""
|
| 349 |
+
Theoretical minimum steps for a perfect agent on this task.
|
| 350 |
+
Used for efficiency scoring in the grader.
|
| 351 |
+
"""
|
| 352 |
+
return {TASK_EASY: 3, TASK_MEDIUM: 5, TASK_HARD: 7}.get(self._task_id, 7)
|
| 353 |
+
|
| 354 |
+
def _terminal_bonus(self, total_improvement: float) -> float:
|
| 355 |
+
"""Bonus reward at episode end based on total optimization level."""
|
| 356 |
+
if total_improvement >= 0.50:
|
| 357 |
+
return 0.6
|
| 358 |
+
elif total_improvement >= 0.40:
|
| 359 |
+
return 0.4
|
| 360 |
+
elif total_improvement >= 0.30:
|
| 361 |
+
return 0.2
|
| 362 |
+
elif total_improvement >= 0.20:
|
| 363 |
+
return 0.05
|
| 364 |
+
elif total_improvement > 0.0:
|
| 365 |
+
return 0.0
|
| 366 |
+
else:
|
| 367 |
+
return -0.2
|
| 368 |
+
|
| 369 |
+
def _make_observation(self, reward: float, done: bool, last_pass_id) -> CompilerOptObservation:
|
| 370 |
+
"""Construct a full CompilerOptObservation from current state."""
|
| 371 |
+
if self._program is None:
|
| 372 |
+
return CompilerOptObservation()
|
| 373 |
+
|
| 374 |
+
name, instructions, loops, branches, functions, loop_depth, _ = self._program
|
| 375 |
+
improvement = self._total_improvement()
|
| 376 |
+
|
| 377 |
+
grader_score = None
|
| 378 |
+
if done:
|
| 379 |
+
grader_score = self.grade()
|
| 380 |
+
|
| 381 |
+
return CompilerOptObservation(
|
| 382 |
+
estimated_cost = round(self._current_cost, 2),
|
| 383 |
+
baseline_cost = round(self._baseline_cost, 2),
|
| 384 |
+
num_instructions = instructions,
|
| 385 |
+
num_loops = loops,
|
| 386 |
+
num_branches = branches,
|
| 387 |
+
num_functions = functions,
|
| 388 |
+
loop_depth = loop_depth,
|
| 389 |
+
program_type = name,
|
| 390 |
+
passes_applied = list(self._passes_applied),
|
| 391 |
+
passes_available = [i for i in range(NUM_PASSES) if i not in self._passes_applied],
|
| 392 |
+
step_count = self._state.step_count,
|
| 393 |
+
max_steps = MAX_STEPS,
|
| 394 |
+
synergy_state = [round(float(x), 3) for x in self._synergy_state],
|
| 395 |
+
task_id = self._task_id,
|
| 396 |
+
task_description = TASK_DESCRIPTIONS[self._task_id],
|
| 397 |
+
done = done,
|
| 398 |
+
reward = round(reward, 4),
|
| 399 |
+
improvement_pct = round(improvement * 100, 2),
|
| 400 |
+
last_pass_name = PASS_NAMES.get(last_pass_id) if last_pass_id is not None else None,
|
| 401 |
+
grader_score = grader_score,
|
| 402 |
+
)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
test_env.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick sanity test for the Compiler Pass Ordering environment.
|
| 3 |
+
Run with: python test_env.py
|
| 4 |
+
Server must be running: uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from compiler_opt_env import CompilerOptAction, CompilerOptEnv
|
| 8 |
+
from compiler_opt_env.models import PASS_NAMES, TASK_EASY, TASK_MEDIUM, TASK_HARD
|
| 9 |
+
from compiler_opt_env.server.compiler_opt_env_environment import BASE_PASS_EFFECTS
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def run_greedy(env, task_id: int, label: str):
|
| 13 |
+
"""Greedy agent: always pick the available pass with highest base effect."""
|
| 14 |
+
print(f"\n{'─'*60}")
|
| 15 |
+
print(f" {label} (greedy agent)")
|
| 16 |
+
print(f"{'─'*60}")
|
| 17 |
+
|
| 18 |
+
obs = env.reset().observation if hasattr(env.reset(), 'observation') else env.reset()
|
| 19 |
+
# reset() returns StepResult in sync mode
|
| 20 |
+
result = env.reset()
|
| 21 |
+
obs = result.observation
|
| 22 |
+
|
| 23 |
+
print(f" Program type: {obs.program_type}")
|
| 24 |
+
print(f" Baseline cost: {obs.baseline_cost:.1f}")
|
| 25 |
+
print()
|
| 26 |
+
|
| 27 |
+
while not obs.done:
|
| 28 |
+
available = obs.passes_available
|
| 29 |
+
best = max(available, key=lambda p: BASE_PASS_EFFECTS[p])
|
| 30 |
+
step_result = env.step(CompilerOptAction(pass_id=best, task_id=task_id))
|
| 31 |
+
obs = step_result.observation
|
| 32 |
+
print(f" Step {obs.step_count:2d}: {PASS_NAMES[best]:35s} "
|
| 33 |
+
f"cost={obs.estimated_cost:7.1f} "
|
| 34 |
+
f"improvement={obs.improvement_pct:5.1f}% "
|
| 35 |
+
f"reward={step_result.reward:+.4f}")
|
| 36 |
+
|
| 37 |
+
print(f"\n Greedy improvement: {obs.improvement_pct:.1f}%")
|
| 38 |
+
print(f" Grader score: {obs.grader_score:.3f}")
|
| 39 |
+
return obs.improvement_pct, obs.grader_score
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def run_optimal_task1(env):
|
| 43 |
+
"""Hand-crafted optimal sequence for Task 1: alias → DCE → vectorization chain."""
|
| 44 |
+
print(f"\n{'─'*60}")
|
| 45 |
+
print(f" Task 1 — Optimal sequence (alias → DCE → vectorization)")
|
| 46 |
+
print(f"{'─'*60}")
|
| 47 |
+
|
| 48 |
+
result = env.reset()
|
| 49 |
+
obs = result.observation
|
| 50 |
+
print(f" Program type: {obs.program_type}")
|
| 51 |
+
print(f" Baseline cost: {obs.baseline_cost:.1f}\n")
|
| 52 |
+
|
| 53 |
+
# FIX: Padded the sequence to 10 steps to ensure the episode finishes (done=True)
|
| 54 |
+
optimal_sequence = [13, 0, 4, 5, 2, 7, 1, 10, 8, 9]
|
| 55 |
+
|
| 56 |
+
for pass_id in optimal_sequence:
|
| 57 |
+
if obs.done:
|
| 58 |
+
break
|
| 59 |
+
step_result = env.step(CompilerOptAction(pass_id=pass_id, task_id=TASK_EASY))
|
| 60 |
+
obs = step_result.observation
|
| 61 |
+
print(f" Step {obs.step_count:2d}: {PASS_NAMES[pass_id]:35s} "
|
| 62 |
+
f"cost={obs.estimated_cost:7.1f} "
|
| 63 |
+
f"improvement={obs.improvement_pct:5.1f}% "
|
| 64 |
+
f"reward={step_result.reward:+.4f}")
|
| 65 |
+
|
| 66 |
+
print(f"\n Optimal improvement: {obs.improvement_pct:.1f}%")
|
| 67 |
+
print(f" Grader score: {obs.grader_score:.3f}")
|
| 68 |
+
return obs.improvement_pct, obs.grader_score
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
print("Compiler Pass Ordering — Environment Sanity Test")
|
| 73 |
+
print("=" * 60)
|
| 74 |
+
|
| 75 |
+
with CompilerOptEnv(base_url="http://localhost:8000").sync() as env:
|
| 76 |
+
|
| 77 |
+
# Task 1: greedy vs optimal
|
| 78 |
+
greedy_improv_1, greedy_score_1 = run_greedy(env, TASK_EASY, "Task 1 (Easy)")
|
| 79 |
+
opt_improv_1, opt_score_1 = run_optimal_task1(env)
|
| 80 |
+
|
| 81 |
+
# Task 2: greedy
|
| 82 |
+
greedy_improv_2, greedy_score_2 = run_greedy(env, TASK_MEDIUM, "Task 2 (Medium)")
|
| 83 |
+
|
| 84 |
+
# Task 3: greedy
|
| 85 |
+
greedy_improv_3, greedy_score_3 = run_greedy(env, TASK_HARD, "Task 3 (Hard)")
|
| 86 |
+
|
| 87 |
+
print(f"\n{'='*60}")
|
| 88 |
+
print(" SUMMARY")
|
| 89 |
+
print(f"{'='*60}")
|
| 90 |
+
print(f" Task 1 greedy: {greedy_improv_1:.1f}% improvement score={greedy_score_1:.3f}")
|
| 91 |
+
print(f" Task 1 optimal: {opt_improv_1:.1f}% improvement score={opt_score_1:.3f}")
|
| 92 |
+
print(f" Task 2 greedy: {greedy_improv_2:.1f}% improvement score={greedy_score_2:.3f}")
|
| 93 |
+
print(f" Task 3 greedy: {greedy_improv_3:.1f}% improvement score={greedy_score_3:.3f}")
|
| 94 |
+
print()
|
| 95 |
+
print(" Expected: greedy ~19-24% | optimal Task 1 ~40-50%")
|
| 96 |
+
print(" If greedy << optimal: ✓ environment requires RL")
|
| 97 |
+
print(f"{'='*60}")
|