Parth3841 commited on
Commit
7c2f148
·
verified ·
1 Parent(s): f99fb2c

Upload folder using huggingface_hub

Browse files
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: Meta Hackathon Main
3
- emoji: 🔥
4
- colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")