rl2xla 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rl2xla-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vishesh Narayan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rl2xla-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: rl2xla
3
+ Version: 0.1.0
4
+ Summary: Automatic compilation of Gymnasium RL environments to JAX/XLA kernels.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/visheshnarayan/jax-accelerated-robot-rl
7
+ Project-URL: Repository, https://github.com/visheshnarayan/jax-accelerated-robot-rl
8
+ Keywords: jax,reinforcement-learning,xla,gymnasium,ppo,compiler
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: gymnasium>=0.29
18
+ Requires-Dist: jax>=0.4.30
19
+ Requires-Dist: flax>=0.8.5
20
+ Requires-Dist: optax>=0.2.3
21
+ Requires-Dist: numpy>=1.26
22
+ Requires-Dist: matplotlib>=3.8
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: ruff>=0.5; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # RL → XLA: Automatic Compilation of Gymnasium RL Environments to JAX Kernels
29
+
30
+ ![Workflow](assets/plots/workflow_v8.png)
31
+
32
+
33
+ Contact-rich robot manipulation is bottlenecked by Python interpreter overhead. Every PPO gradient update requires thousands of env steps, all gated by `for` loops that stall the CPU waiting for the interpreter instead of computing. This project eliminates those bottlenecks by compiling the full training loop into JAX/XLA — and ships two tools that let you do it for **any Gymnasium env** without rewriting it by hand.
34
+
35
+ We developed two components:
36
+
37
+ - **`gym_to_jax`** — an AST-based converter that takes any NumPy Gymnasium env and produces `vmap`/`scan`-safe pure JAX functions automatically, via a pipeline of 9 source transformers
38
+ - **`compile_ppo`** — a one-call framework that wraps any `(reset_fn, step_fn)` pair into three compiled `@jax.jit` kernels covering rollout collection, GAE, and minibatch updates — zero `for`-loops written by the caller
39
+
40
+ Together they take a standard NumPy Gymnasium env to a 22× faster compiled training loop with a three-line call.
41
+
42
+ ---
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ pip install rl2xla
48
+ ```
49
+
50
+ ```python
51
+ from flax import linen as nn
52
+ from rl2xla import gym_to_jax, compile_ppo, PPOConfig
53
+ import jax.numpy as jnp
54
+
55
+ # 1. Define your ActorCritic network
56
+ class ActorCritic(nn.Module):
57
+ action_dim: int
58
+ @nn.compact
59
+ def __call__(self, obs):
60
+ x = nn.tanh(nn.Dense(64)(obs))
61
+ x = nn.tanh(nn.Dense(64)(x))
62
+ mean = nn.Dense(self.action_dim)(x)
63
+ log_std = self.param('log_std', nn.initializers.zeros, (self.action_dim,))
64
+ value = nn.Dense(1)(x)[..., 0]
65
+ return mean, jnp.broadcast_to(log_std, mean.shape), value
66
+
67
+ # 2. Convert any Gymnasium env by name
68
+ reset_fn, step_fn = gym_to_jax("MountainCarContinuous-v0")
69
+
70
+ # 3. Compile and train — obs_dim / action_dim inferred automatically
71
+ trainer = compile_ppo(reset_fn, step_fn, ActorCritic(action_dim=1))
72
+
73
+ result = trainer.train(PPOConfig(num_envs=256, updates=300), seed=0)
74
+ print(f"{result['wall_time_s']:.1f}s — {result['steps_per_second']:,} steps/s")
75
+ ```
76
+
77
+ Works out of the box with **CartPole-v1**, **MountainCar-v0**, **MountainCarContinuous-v0**, **Pendulum-v1**, **MicroduckWalkEnv** (14-DOF bipedal, 61-dim obs), and any custom env with pure Python + NumPy logic.
78
+
79
+ ---
80
+
81
+ ## Framework
82
+
83
+ ```
84
+ NumPy Gymnasium env
85
+
86
+ ▼ gym_to_jax() [9 AST transformers → pure JAX functions]
87
+ (reset_fn, step_fn)
88
+
89
+ ▼ compile_ppo() [jax.jit + lax.scan compilation]
90
+ PPOTrainer
91
+ ├── collect() jax.jit — lax.scan over H steps, vmap over N envs, auto-reset
92
+ ├── gae() jax.jit — lax.scan(reverse=True) advantage estimation
93
+ └── run_epoch() jax.jit — lax.scan over shuffled minibatches per epoch
94
+ ```
95
+
96
+ ```python
97
+ from rl2xla.gym_to_jax import gym_to_jax
98
+ from rl2xla.jax_convert import compile_ppo, PPOConfig
99
+
100
+ reset_fn, step_fn = gym_to_jax(MyEnv) # convert
101
+ trainer = compile_ppo(reset_fn, step_fn, net, obs_dim=4, action_dim=2) # compile
102
+ result = trainer.train(PPOConfig(num_envs=256, updates=300), seed=0) # train
103
+ # → {"wall_time_s": 5.2, "steps_per_second": 118500} (22× vs Python baseline)
104
+ ```
105
+
106
+ ### `gym_to_jax` — AST conversion pipeline
107
+
108
+ ![AST pipeline](assets/plots/ast_pipeline.png)
109
+
110
+ `gym_to_jax` parses the source of any NumPy Gymnasium env, applies nine AST transformers, and `exec`s the result to produce a `(reset_fn, step_fn)` pair compatible with `jax.jit`, `jax.vmap`, and `jax.lax.scan`. State structure is discovered automatically by probing `env.__dict__` after `reset()`.
111
+
112
+ **Before** (standard NumPy Gymnasium env):
113
+ ```python
114
+ def step(self, action):
115
+ self.vx += action[0] * self.dt # augmented assign
116
+ self.x += self.vx * self.dt
117
+ dist = np.linalg.norm(self.x - self.goal)
118
+ done = bool(dist < 0.1) # Python cast
119
+ reward = float(dist < 0.1)
120
+ return self._obs(), reward, done, {}
121
+ ```
122
+
123
+ **After** (auto-generated — `vmap`/`scan`-safe):
124
+ ```python
125
+ def step_fn(state, action):
126
+ vx = state.vx + action[0] * dt # AugAssign → rebind
127
+ x = state.x + vx * dt
128
+ dist = jnp.linalg.norm(x - goal) # np → jnp
129
+ done = dist < 0.1 # bool() removed
130
+ reward = jnp.where(done, 1.0, 0.0) # ternary → jnp.where
131
+ return AutoEnvState(x=x, vx=vx), obs_fn(x), reward, done
132
+ ```
133
+
134
+ | Transformer | Before | After |
135
+ |---|---|---|
136
+ | `_NpToJnp` | `np.linalg.norm(x)` | `jnp.linalg.norm(x)` |
137
+ | `_RemovePyCasts` | `bool(x)`, `float(x)` | `x` |
138
+ | `_AugAssignToAssign` | `x += y` | `x = x + y` |
139
+ | `_SliceAssignToRebind` | `x[:] = y` | `x = y` |
140
+ | `_IfToWhere` | `if cond: x = y` | `x = jnp.where(cond, y, x)` |
141
+ | `_TernaryToWhere` | `a if c else b` | `jnp.where(c, a, b)` |
142
+ | `_BoolOpToJax` | `a or b` / `a and b` | `a \| b` / `a & b` |
143
+ | `_NpRandomToJax` | `self.np_random.uniform(lo, hi)` | `jax.random.uniform(key, minval=lo, maxval=hi)` |
144
+ | `_SelfFieldToLocal` | `self.x`, `self.vx` | local vars packed into `AutoEnvState` NamedTuple |
145
+
146
+ **Compatibility:** pure Python + NumPy logic with separate `self.field` state vars. Envs with C++ backends (MuJoCo, Box2D) cannot be auto-converted.
147
+
148
+ ### `compile_ppo` — three compiled XLA kernels
149
+
150
+ **Kernel 1 — `collect(states, obs, params, key)`**
151
+
152
+ Runs `H` steps across `N` parallel envs as a single `lax.scan`. Inside, `jax.vmap(step_fn)` batches all envs into one SIMD call. Done envs are reset functionally via `jnp.where` — no Python branching.
153
+
154
+ ```python
155
+ (states, obs, params), traj = jax.lax.scan(step_body, carry, None, length=H)
156
+ # traj: _Trajectory buffer shaped (H, N, …)
157
+ ```
158
+
159
+ **Kernel 2 — `gae(rewards, values, dones, last_value)`**
160
+
161
+ GAE backward pass as `lax.scan(reverse=True)` — no Python backward loop.
162
+
163
+ ```
164
+ δₜ = rₜ + γ·V(sₜ₊₁)·(1−dₜ) − V(sₜ)
165
+ Aₜ = δₜ + γλ·(1−dₜ)·Aₜ₊₁
166
+ ```
167
+
168
+ ```python
169
+ _, adv = jax.lax.scan(gae_step, (zeros, last_value), (rewards, values, dones), reverse=True)
170
+ ```
171
+
172
+ **Kernel 3 — `run_epoch(params, opt_state, key, obs, actions, …)`**
173
+
174
+ Shuffles the flattened trajectory and runs one PPO epoch across all minibatches via `lax.scan`. Each minibatch computes clipped surrogate loss + value loss + entropy bonus and applies an Adam step.
175
+
176
+ ```python
177
+ (params, opt_state), losses = jax.lax.scan(_update_mb, (params, opt_state), minibatches)
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Results
183
+
184
+ ![Speedup bar charts](assets/plots/speedup_bars.png)
185
+
186
+ ![Pipeline diagram](assets/plots/pipeline_diagram.png)
187
+
188
+ ### End-to-end training (16 envs, 300 updates, H=128)
189
+
190
+ | Implementation | Wall time | Steps/s | Speedup |
191
+ |---|---|---|---|
192
+ | Tier 2 — Python loops + NumPy env | 113 s | 5,440 | 1× |
193
+ | Tier 4 — `lax.scan` + `vmap` JAX env | 6.7 s | 91,700 | 17× |
194
+ | `compile_ppo` framework | **5.2 s** | 118,500 | **22×** |
195
+
196
+ All reach **100% task success**. The speedup is lossless.
197
+
198
+ The 17× end-to-end vs 7.8× rollout-only gap comes from the GAE backward pass and minibatch loop each also being compiled — eliminating just one loop undersells the gain.
199
+
200
+ ### Throughput scaling (rollout collection only)
201
+
202
+ | Envs | Tier 2 steps/s | Tier 4 steps/s | Speedup |
203
+ |---|---|---|---|
204
+ | 8 | 62,049 | 423,020 | 6.8× |
205
+ | 16 | 78,344 | 609,976 | 7.8× |
206
+ | 64 | 97,444 | 1,098,652 | 11.3× |
207
+ | 128 | 101,447 | 1,283,713 | **12.7×** |
208
+
209
+ Tier 2 plateaus at ~100K steps/s — the Python loop is O(N) serial. Tier 4 scales linearly because `vmap` batches all N envs into one XLA SIMD kernel.
210
+
211
+ ### bfloat16 mixed precision (256 envs, 100 updates)
212
+
213
+ | Dtype | Wall time | Steps/s | Success |
214
+ |---|---|---|---|
215
+ | float32 | 24.8 s | 132,000 | 100% |
216
+ | bfloat16 | 32.2 s | 101,700 | 100% |
217
+
218
+ **bf16 is 30% slower on CPU** — x86 has no native bf16 compute units. On GPU (A100/H100 Tensor Cores) the same code yields ~1.5–2× speedup instead.
219
+
220
+ ---
221
+
222
+ ## Why robotics
223
+
224
+ The task is a contact-rich cube-push, a minimal proxy for the Franka Panda manipulation stack shown above. Fast RL iteration (22× per training run) makes hyperparameter sweeps and architecture searches feasible at robot-scale environment counts (256–1024 parallel sims). `gym_to_jax` + `compile_ppo` bring that speed to any NumPy Gymnasium env without manual rewriting. Plugging a GPU-accelerated Isaac Sim step into the same `vmap`/`scan` harness is the natural next step.
225
+
226
+ ---
227
+
228
+ ## Throughput tiers
229
+
230
+ ```
231
+ Tier 2 NumPy env + Python loops baseline — matches CleanRL / SB3 style
232
+ Tier 3 JAX env + Python rollout vmap over envs only; slower than T2 at low N
233
+ Tier 4 JAX env + lax.scan rollout fully compiled; one XLA kernel per update
234
+ Tier 4b Tier 4 + bfloat16 mixed precision; GPU benefit only
235
+ compile_ppo gym_to_jax → compile_ppo zero for-loops; any NumPy Gymnasium env
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Quickstart
241
+
242
+ ```bash
243
+ python -m venv .venv && source .venv/bin/activate
244
+ pip install -e '.[dev]'
245
+
246
+ # Tier 2 baseline (Python / NumPy env)
247
+ python scripts/train_ppo.py --seed 0 --updates 300 --envs 16
248
+
249
+ # Tier 4 (fully compiled, 1024 parallel envs)
250
+ python scripts/train_ppo_scan.py --seed 0 --updates 300 --num-envs 1024
251
+
252
+ # gym_to_jax + compile_ppo (one-call automatic pipeline)
253
+ python scripts/train_ppo_auto.py --seed 0 --updates 300 --num-envs 256
254
+
255
+ # Throughput benchmark across all tiers
256
+ python scripts/benchmark_throughput.py --steps 2000000 --envs 128
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Project layout
262
+
263
+ ```
264
+ rl2xla/
265
+ env.py Tier 2 NumPy Gymnasium env (Python-loopable)
266
+ jax_env.py Tier 4 env — pure-JAX stateless (vmap/scan-safe)
267
+ jax_convert.py compile_ppo — 3 compiled XLA kernels + PPOConfig
268
+ gym_to_jax.py gym_to_jax — 9 AST transformers, state probing, AutoEnvState
269
+ test_env.py NavEnv — 2D navigation env (exercises general AST path)
270
+ microduck_env.py MicroduckWalkEnv — 14-DOF bipedal env, 61-dim obs, 14-dim action
271
+ world_model.py Flax/Optax action-conditioned dynamics model
272
+ planner.py CEM planning over imagined rollouts
273
+
274
+ scripts/
275
+ train_ppo.py Tier 2 PPO training
276
+ train_ppo_scan.py Tier 4 PPO with vmap env + lax.scan rollout/GAE/minibatch
277
+ train_ppo_compiled.py compile_ppo with hand-written JAX env
278
+ train_ppo_auto.py gym_to_jax → compile_ppo end-to-end demo
279
+ test_gym_to_jax.py Conversion tests for PushCubeEnv and NavEnv
280
+ benchmark_throughput.py Tier 2 / 3 / 4 rollout throughput comparison
281
+
282
+ reports/
283
+ findings.md Full experimental findings with methodology notes
284
+ experiments_log.md Append-only log of all runs
285
+
286
+ paper/
287
+ main.tex 2-column paper: JAX-Accelerated Robot RL
288
+ pipeline_diagram.tex TikZ diagram of PPO loop bottlenecks and JAX optimisations
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Roadmap
294
+
295
+ - [ ] Reproduce Tier 2→4 speedup on GPU (expected 100×+ gap)
296
+ - [ ] Tier 5: scan over updates for a fully compiled outer loop
297
+ - [ ] Extend `gym_to_jax` to handle `elif` chains and while loops
298
+ - [ ] Train on `MicroduckWalkEnv` and benchmark against microduck_rl (PyTorch/MuJoCo Warp baseline)
299
+ - [ ] Connect learned controller to Isaac Lab Panda task
300
+ - [ ] Pixel observations + language-conditioned commands
rl2xla-0.1.0/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # RL → XLA: Automatic Compilation of Gymnasium RL Environments to JAX Kernels
2
+
3
+ ![Workflow](assets/plots/workflow_v8.png)
4
+
5
+
6
+ Contact-rich robot manipulation is bottlenecked by Python interpreter overhead. Every PPO gradient update requires thousands of env steps, all gated by `for` loops that stall the CPU waiting for the interpreter instead of computing. This project eliminates those bottlenecks by compiling the full training loop into JAX/XLA — and ships two tools that let you do it for **any Gymnasium env** without rewriting it by hand.
7
+
8
+ We developed two components:
9
+
10
+ - **`gym_to_jax`** — an AST-based converter that takes any NumPy Gymnasium env and produces `vmap`/`scan`-safe pure JAX functions automatically, via a pipeline of 9 source transformers
11
+ - **`compile_ppo`** — a one-call framework that wraps any `(reset_fn, step_fn)` pair into three compiled `@jax.jit` kernels covering rollout collection, GAE, and minibatch updates — zero `for`-loops written by the caller
12
+
13
+ Together they take a standard NumPy Gymnasium env to a 22× faster compiled training loop with a three-line call.
14
+
15
+ ---
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ pip install rl2xla
21
+ ```
22
+
23
+ ```python
24
+ from flax import linen as nn
25
+ from rl2xla import gym_to_jax, compile_ppo, PPOConfig
26
+ import jax.numpy as jnp
27
+
28
+ # 1. Define your ActorCritic network
29
+ class ActorCritic(nn.Module):
30
+ action_dim: int
31
+ @nn.compact
32
+ def __call__(self, obs):
33
+ x = nn.tanh(nn.Dense(64)(obs))
34
+ x = nn.tanh(nn.Dense(64)(x))
35
+ mean = nn.Dense(self.action_dim)(x)
36
+ log_std = self.param('log_std', nn.initializers.zeros, (self.action_dim,))
37
+ value = nn.Dense(1)(x)[..., 0]
38
+ return mean, jnp.broadcast_to(log_std, mean.shape), value
39
+
40
+ # 2. Convert any Gymnasium env by name
41
+ reset_fn, step_fn = gym_to_jax("MountainCarContinuous-v0")
42
+
43
+ # 3. Compile and train — obs_dim / action_dim inferred automatically
44
+ trainer = compile_ppo(reset_fn, step_fn, ActorCritic(action_dim=1))
45
+
46
+ result = trainer.train(PPOConfig(num_envs=256, updates=300), seed=0)
47
+ print(f"{result['wall_time_s']:.1f}s — {result['steps_per_second']:,} steps/s")
48
+ ```
49
+
50
+ Works out of the box with **CartPole-v1**, **MountainCar-v0**, **MountainCarContinuous-v0**, **Pendulum-v1**, **MicroduckWalkEnv** (14-DOF bipedal, 61-dim obs), and any custom env with pure Python + NumPy logic.
51
+
52
+ ---
53
+
54
+ ## Framework
55
+
56
+ ```
57
+ NumPy Gymnasium env
58
+
59
+ ▼ gym_to_jax() [9 AST transformers → pure JAX functions]
60
+ (reset_fn, step_fn)
61
+
62
+ ▼ compile_ppo() [jax.jit + lax.scan compilation]
63
+ PPOTrainer
64
+ ├── collect() jax.jit — lax.scan over H steps, vmap over N envs, auto-reset
65
+ ├── gae() jax.jit — lax.scan(reverse=True) advantage estimation
66
+ └── run_epoch() jax.jit — lax.scan over shuffled minibatches per epoch
67
+ ```
68
+
69
+ ```python
70
+ from rl2xla.gym_to_jax import gym_to_jax
71
+ from rl2xla.jax_convert import compile_ppo, PPOConfig
72
+
73
+ reset_fn, step_fn = gym_to_jax(MyEnv) # convert
74
+ trainer = compile_ppo(reset_fn, step_fn, net, obs_dim=4, action_dim=2) # compile
75
+ result = trainer.train(PPOConfig(num_envs=256, updates=300), seed=0) # train
76
+ # → {"wall_time_s": 5.2, "steps_per_second": 118500} (22× vs Python baseline)
77
+ ```
78
+
79
+ ### `gym_to_jax` — AST conversion pipeline
80
+
81
+ ![AST pipeline](assets/plots/ast_pipeline.png)
82
+
83
+ `gym_to_jax` parses the source of any NumPy Gymnasium env, applies nine AST transformers, and `exec`s the result to produce a `(reset_fn, step_fn)` pair compatible with `jax.jit`, `jax.vmap`, and `jax.lax.scan`. State structure is discovered automatically by probing `env.__dict__` after `reset()`.
84
+
85
+ **Before** (standard NumPy Gymnasium env):
86
+ ```python
87
+ def step(self, action):
88
+ self.vx += action[0] * self.dt # augmented assign
89
+ self.x += self.vx * self.dt
90
+ dist = np.linalg.norm(self.x - self.goal)
91
+ done = bool(dist < 0.1) # Python cast
92
+ reward = float(dist < 0.1)
93
+ return self._obs(), reward, done, {}
94
+ ```
95
+
96
+ **After** (auto-generated — `vmap`/`scan`-safe):
97
+ ```python
98
+ def step_fn(state, action):
99
+ vx = state.vx + action[0] * dt # AugAssign → rebind
100
+ x = state.x + vx * dt
101
+ dist = jnp.linalg.norm(x - goal) # np → jnp
102
+ done = dist < 0.1 # bool() removed
103
+ reward = jnp.where(done, 1.0, 0.0) # ternary → jnp.where
104
+ return AutoEnvState(x=x, vx=vx), obs_fn(x), reward, done
105
+ ```
106
+
107
+ | Transformer | Before | After |
108
+ |---|---|---|
109
+ | `_NpToJnp` | `np.linalg.norm(x)` | `jnp.linalg.norm(x)` |
110
+ | `_RemovePyCasts` | `bool(x)`, `float(x)` | `x` |
111
+ | `_AugAssignToAssign` | `x += y` | `x = x + y` |
112
+ | `_SliceAssignToRebind` | `x[:] = y` | `x = y` |
113
+ | `_IfToWhere` | `if cond: x = y` | `x = jnp.where(cond, y, x)` |
114
+ | `_TernaryToWhere` | `a if c else b` | `jnp.where(c, a, b)` |
115
+ | `_BoolOpToJax` | `a or b` / `a and b` | `a \| b` / `a & b` |
116
+ | `_NpRandomToJax` | `self.np_random.uniform(lo, hi)` | `jax.random.uniform(key, minval=lo, maxval=hi)` |
117
+ | `_SelfFieldToLocal` | `self.x`, `self.vx` | local vars packed into `AutoEnvState` NamedTuple |
118
+
119
+ **Compatibility:** pure Python + NumPy logic with separate `self.field` state vars. Envs with C++ backends (MuJoCo, Box2D) cannot be auto-converted.
120
+
121
+ ### `compile_ppo` — three compiled XLA kernels
122
+
123
+ **Kernel 1 — `collect(states, obs, params, key)`**
124
+
125
+ Runs `H` steps across `N` parallel envs as a single `lax.scan`. Inside, `jax.vmap(step_fn)` batches all envs into one SIMD call. Done envs are reset functionally via `jnp.where` — no Python branching.
126
+
127
+ ```python
128
+ (states, obs, params), traj = jax.lax.scan(step_body, carry, None, length=H)
129
+ # traj: _Trajectory buffer shaped (H, N, …)
130
+ ```
131
+
132
+ **Kernel 2 — `gae(rewards, values, dones, last_value)`**
133
+
134
+ GAE backward pass as `lax.scan(reverse=True)` — no Python backward loop.
135
+
136
+ ```
137
+ δₜ = rₜ + γ·V(sₜ₊₁)·(1−dₜ) − V(sₜ)
138
+ Aₜ = δₜ + γλ·(1−dₜ)·Aₜ₊₁
139
+ ```
140
+
141
+ ```python
142
+ _, adv = jax.lax.scan(gae_step, (zeros, last_value), (rewards, values, dones), reverse=True)
143
+ ```
144
+
145
+ **Kernel 3 — `run_epoch(params, opt_state, key, obs, actions, …)`**
146
+
147
+ Shuffles the flattened trajectory and runs one PPO epoch across all minibatches via `lax.scan`. Each minibatch computes clipped surrogate loss + value loss + entropy bonus and applies an Adam step.
148
+
149
+ ```python
150
+ (params, opt_state), losses = jax.lax.scan(_update_mb, (params, opt_state), minibatches)
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Results
156
+
157
+ ![Speedup bar charts](assets/plots/speedup_bars.png)
158
+
159
+ ![Pipeline diagram](assets/plots/pipeline_diagram.png)
160
+
161
+ ### End-to-end training (16 envs, 300 updates, H=128)
162
+
163
+ | Implementation | Wall time | Steps/s | Speedup |
164
+ |---|---|---|---|
165
+ | Tier 2 — Python loops + NumPy env | 113 s | 5,440 | 1× |
166
+ | Tier 4 — `lax.scan` + `vmap` JAX env | 6.7 s | 91,700 | 17× |
167
+ | `compile_ppo` framework | **5.2 s** | 118,500 | **22×** |
168
+
169
+ All reach **100% task success**. The speedup is lossless.
170
+
171
+ The 17× end-to-end vs 7.8× rollout-only gap comes from the GAE backward pass and minibatch loop each also being compiled — eliminating just one loop undersells the gain.
172
+
173
+ ### Throughput scaling (rollout collection only)
174
+
175
+ | Envs | Tier 2 steps/s | Tier 4 steps/s | Speedup |
176
+ |---|---|---|---|
177
+ | 8 | 62,049 | 423,020 | 6.8× |
178
+ | 16 | 78,344 | 609,976 | 7.8× |
179
+ | 64 | 97,444 | 1,098,652 | 11.3× |
180
+ | 128 | 101,447 | 1,283,713 | **12.7×** |
181
+
182
+ Tier 2 plateaus at ~100K steps/s — the Python loop is O(N) serial. Tier 4 scales linearly because `vmap` batches all N envs into one XLA SIMD kernel.
183
+
184
+ ### bfloat16 mixed precision (256 envs, 100 updates)
185
+
186
+ | Dtype | Wall time | Steps/s | Success |
187
+ |---|---|---|---|
188
+ | float32 | 24.8 s | 132,000 | 100% |
189
+ | bfloat16 | 32.2 s | 101,700 | 100% |
190
+
191
+ **bf16 is 30% slower on CPU** — x86 has no native bf16 compute units. On GPU (A100/H100 Tensor Cores) the same code yields ~1.5–2× speedup instead.
192
+
193
+ ---
194
+
195
+ ## Why robotics
196
+
197
+ The task is a contact-rich cube-push, a minimal proxy for the Franka Panda manipulation stack shown above. Fast RL iteration (22× per training run) makes hyperparameter sweeps and architecture searches feasible at robot-scale environment counts (256–1024 parallel sims). `gym_to_jax` + `compile_ppo` bring that speed to any NumPy Gymnasium env without manual rewriting. Plugging a GPU-accelerated Isaac Sim step into the same `vmap`/`scan` harness is the natural next step.
198
+
199
+ ---
200
+
201
+ ## Throughput tiers
202
+
203
+ ```
204
+ Tier 2 NumPy env + Python loops baseline — matches CleanRL / SB3 style
205
+ Tier 3 JAX env + Python rollout vmap over envs only; slower than T2 at low N
206
+ Tier 4 JAX env + lax.scan rollout fully compiled; one XLA kernel per update
207
+ Tier 4b Tier 4 + bfloat16 mixed precision; GPU benefit only
208
+ compile_ppo gym_to_jax → compile_ppo zero for-loops; any NumPy Gymnasium env
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Quickstart
214
+
215
+ ```bash
216
+ python -m venv .venv && source .venv/bin/activate
217
+ pip install -e '.[dev]'
218
+
219
+ # Tier 2 baseline (Python / NumPy env)
220
+ python scripts/train_ppo.py --seed 0 --updates 300 --envs 16
221
+
222
+ # Tier 4 (fully compiled, 1024 parallel envs)
223
+ python scripts/train_ppo_scan.py --seed 0 --updates 300 --num-envs 1024
224
+
225
+ # gym_to_jax + compile_ppo (one-call automatic pipeline)
226
+ python scripts/train_ppo_auto.py --seed 0 --updates 300 --num-envs 256
227
+
228
+ # Throughput benchmark across all tiers
229
+ python scripts/benchmark_throughput.py --steps 2000000 --envs 128
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Project layout
235
+
236
+ ```
237
+ rl2xla/
238
+ env.py Tier 2 NumPy Gymnasium env (Python-loopable)
239
+ jax_env.py Tier 4 env — pure-JAX stateless (vmap/scan-safe)
240
+ jax_convert.py compile_ppo — 3 compiled XLA kernels + PPOConfig
241
+ gym_to_jax.py gym_to_jax — 9 AST transformers, state probing, AutoEnvState
242
+ test_env.py NavEnv — 2D navigation env (exercises general AST path)
243
+ microduck_env.py MicroduckWalkEnv — 14-DOF bipedal env, 61-dim obs, 14-dim action
244
+ world_model.py Flax/Optax action-conditioned dynamics model
245
+ planner.py CEM planning over imagined rollouts
246
+
247
+ scripts/
248
+ train_ppo.py Tier 2 PPO training
249
+ train_ppo_scan.py Tier 4 PPO with vmap env + lax.scan rollout/GAE/minibatch
250
+ train_ppo_compiled.py compile_ppo with hand-written JAX env
251
+ train_ppo_auto.py gym_to_jax → compile_ppo end-to-end demo
252
+ test_gym_to_jax.py Conversion tests for PushCubeEnv and NavEnv
253
+ benchmark_throughput.py Tier 2 / 3 / 4 rollout throughput comparison
254
+
255
+ reports/
256
+ findings.md Full experimental findings with methodology notes
257
+ experiments_log.md Append-only log of all runs
258
+
259
+ paper/
260
+ main.tex 2-column paper: JAX-Accelerated Robot RL
261
+ pipeline_diagram.tex TikZ diagram of PPO loop bottlenecks and JAX optimisations
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Roadmap
267
+
268
+ - [ ] Reproduce Tier 2→4 speedup on GPU (expected 100×+ gap)
269
+ - [ ] Tier 5: scan over updates for a fully compiled outer loop
270
+ - [ ] Extend `gym_to_jax` to handle `elif` chains and while loops
271
+ - [ ] Train on `MicroduckWalkEnv` and benchmark against microduck_rl (PyTorch/MuJoCo Warp baseline)
272
+ - [ ] Connect learned controller to Isaac Lab Panda task
273
+ - [ ] Pixel observations + language-conditioned commands
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rl2xla"
7
+ version = "0.1.0"
8
+ description = "Automatic compilation of Gymnasium RL environments to JAX/XLA kernels."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["jax", "reinforcement-learning", "xla", "gymnasium", "ppo", "compiler"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.11",
19
+ ]
20
+ dependencies = [
21
+ "gymnasium>=0.29",
22
+ "jax>=0.4.30",
23
+ "flax>=0.8.5",
24
+ "optax>=0.2.3",
25
+ "numpy>=1.26",
26
+ "matplotlib>=3.8",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=8", "ruff>=0.5"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/visheshnarayan/jax-accelerated-robot-rl"
34
+ Repository = "https://github.com/visheshnarayan/jax-accelerated-robot-rl"
35
+
36
+ [tool.setuptools.packages.find]
37
+ include = ["rl2xla*"]
38
+
39
+ [tool.ruff]
40
+ line-length = 100
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
@@ -0,0 +1,29 @@
1
+ """
2
+ rl2xla — Automatic compilation of Gymnasium RL environments to JAX/XLA kernels.
3
+
4
+ Public API
5
+ ----------
6
+ gym_to_jax(env_class)
7
+ Convert any NumPy Gymnasium env to pure-JAX (reset_fn, step_fn).
8
+
9
+ compile_ppo(reset_fn, step_fn, net, obs_dim, action_dim)
10
+ Compile a fully JAX-accelerated PPO trainer from two pure functions.
11
+ Returns a PPOTrainer with three compiled @jax.jit kernels.
12
+
13
+ PPOConfig
14
+ NamedTuple of PPO hyperparameters (num_envs, horizon, updates, …).
15
+
16
+ Example
17
+ -------
18
+ from rl2xla import gym_to_jax, compile_ppo, PPOConfig
19
+
20
+ reset_fn, step_fn = gym_to_jax(MyEnv)
21
+ trainer = compile_ppo(reset_fn, step_fn, net, obs_dim=4, action_dim=2)
22
+ result = trainer.train(PPOConfig(num_envs=256, updates=300), seed=0)
23
+ """
24
+
25
+ from rl2xla.gym_to_jax import gym_to_jax, ConversionError
26
+ from rl2xla.jax_convert import compile_ppo, PPOConfig, PPOTrainer
27
+
28
+ __version__ = "0.1.0"
29
+ __all__ = ["gym_to_jax", "ConversionError", "compile_ppo", "PPOConfig", "PPOTrainer"]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+
4
+ def cem_action(env, rng, horizon=8, candidates=64, iterations=3, elite_fraction=.1):
5
+ """Cross-Entropy Method planning entirely inside the learned dynamics env."""
6
+ low, high = env.action_space.low, env.action_space.high; dim=low.shape[0]; mean=np.zeros((horizon,dim),np.float32); std=np.broadcast_to((high-low)/2,(horizon,dim)).copy()
7
+ for _ in range(iterations):
8
+ sequences=np.clip(rng.normal(mean,std,(candidates,horizon,dim)),low,high).astype(np.float32); states=np.repeat(env.state[None],candidates,axis=0); scores=np.zeros(candidates,np.float32)
9
+ for step in range(horizon):
10
+ states=env.model.predict_batch(states,sequences[:,step]); scores-=env._distance_batch(states)
11
+ elite=sequences[np.argsort(scores)[-max(2,int(candidates*elite_fraction)):]]; mean=elite.mean(0); std=np.maximum(elite.std(0),.05*(high-low))
12
+ return np.clip(mean[0],low,high).astype(np.float32)