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 +21 -0
- rl2xla-0.1.0/PKG-INFO +300 -0
- rl2xla-0.1.0/README.md +273 -0
- rl2xla-0.1.0/pyproject.toml +43 -0
- rl2xla-0.1.0/rl2xla/__init__.py +29 -0
- rl2xla-0.1.0/rl2xla/cem.py +12 -0
- rl2xla-0.1.0/rl2xla/env.py +36 -0
- rl2xla-0.1.0/rl2xla/gym_to_jax.py +1596 -0
- rl2xla-0.1.0/rl2xla/isaac_bridge.py +87 -0
- rl2xla-0.1.0/rl2xla/jax_convert.py +356 -0
- rl2xla-0.1.0/rl2xla/jax_env.py +135 -0
- rl2xla-0.1.0/rl2xla/metrics.py +54 -0
- rl2xla-0.1.0/rl2xla/microduck_env.py +273 -0
- rl2xla-0.1.0/rl2xla/model_env.py +26 -0
- rl2xla-0.1.0/rl2xla/planner.py +38 -0
- rl2xla-0.1.0/rl2xla/policy.py +31 -0
- rl2xla-0.1.0/rl2xla/preprocess.py +21 -0
- rl2xla-0.1.0/rl2xla/real_data.py +36 -0
- rl2xla-0.1.0/rl2xla/replay.py +15 -0
- rl2xla-0.1.0/rl2xla/tabular_world_model.py +36 -0
- rl2xla-0.1.0/rl2xla/task.py +6 -0
- rl2xla-0.1.0/rl2xla/test_env.py +46 -0
- rl2xla-0.1.0/rl2xla/world_model.py +43 -0
- rl2xla-0.1.0/rl2xla.egg-info/PKG-INFO +300 -0
- rl2xla-0.1.0/rl2xla.egg-info/SOURCES.txt +32 -0
- rl2xla-0.1.0/rl2xla.egg-info/dependency_links.txt +1 -0
- rl2xla-0.1.0/rl2xla.egg-info/requires.txt +10 -0
- rl2xla-0.1.0/rl2xla.egg-info/top_level.txt +1 -0
- rl2xla-0.1.0/setup.cfg +4 -0
- rl2xla-0.1.0/tests/test_env.py +6 -0
- rl2xla-0.1.0/tests/test_isaac_bridge.py +42 -0
- rl2xla-0.1.0/tests/test_real_data.py +17 -0
- rl2xla-0.1.0/tests/test_rl2xla.py +287 -0
- rl2xla-0.1.0/tests/test_task.py +8 -0
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
185
|
+
|
|
186
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
158
|
+
|
|
159
|
+

|
|
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)
|