Skip to content

Commit 83bf86c

Browse files
committed
Implement rollout buffer
1 parent 245bb73 commit 83bf86c

2 files changed

Lines changed: 163 additions & 20 deletions

File tree

oryx/buffers/base_buffer.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import equinox as eqx
1+
from __future__ import annotations
2+
3+
from abc import abstractmethod
24

3-
from oryx.spaces import AbstractSpace
5+
import equinox as eqx
46

57

68
class AbstractBuffer(eqx.Module, strict=True):
79
"""Base class for buffers."""
810

9-
observation_space: eqx.AbstractVar[AbstractSpace]
11+
@property
12+
@abstractmethod
13+
def shape(self) -> tuple[int, ...]:
14+
"""Return the shape of the buffer."""

oryx/buffers/rollout.py

Lines changed: 155 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,166 @@
11
from __future__ import annotations
22

3-
from jaxtyping import Array
3+
import dataclasses
4+
from functools import partial
45

5-
from oryx.spaces import AbstractSpace
6+
import equinox as eqx
7+
import jax
8+
from jax import lax
9+
from jax import numpy as jnp
10+
from jax import random as jr
11+
from jaxtyping import Array, ArrayLike, Bool, Float, Key, PyTree
612

713
from .base_buffer import AbstractBuffer
814

915

10-
class RolloutBuffer(AbstractBuffer, strict=True):
16+
class RolloutBuffer[ActType, ObsType](AbstractBuffer, strict=True):
17+
"""
18+
RolloutBuffer used by on-policy algorithms.
1119
12-
observation_space: AbstractSpace
13-
observations: Array
14-
actions: Array
15-
rewards: Array
16-
advantages: Array
17-
returns: Array
18-
episode_starts: Array
19-
log_probs: Array
20-
values: Array
20+
Designed for scans and JIT compilation.
2121
22-
def reset(self) -> RolloutBuffer:
23-
pass
22+
Example:
23+
```python
24+
from jax import lax
2425
26+
def step(carry, x) -> RolloutBuffer:
27+
# Do stuff with the carry and x
28+
return carry, RolloutBuffer(
29+
# Variable assignments here
30+
)
31+
32+
rollout_buffer = lax.scan(step, init, xs) # Collect a filled buffer
33+
rollout_buffer = rollout_buffer.compute_returns_and_advantages(
34+
carry.value,
35+
carry.done,
36+
1.0,
37+
0.99,
38+
)
39+
```
40+
"""
41+
42+
observations: PyTree[ObsType]
43+
actions: PyTree[ActType]
44+
rewards: Float[Array, " *size"]
45+
terminations: Bool[Array, " *size"]
46+
truncations: Bool[Array, " *size"]
47+
log_probs: Float[Array, " *size"]
48+
values: Float[Array, " *size"]
49+
returns: Float[Array, " *size"]
50+
advantages: Float[Array, " *size"]
51+
states: PyTree[eqx.nn.State]
52+
53+
def __init__(
54+
self,
55+
observations: PyTree[ObsType],
56+
actions: PyTree[ActType],
57+
rewards: Float[ArrayLike, " *size"],
58+
terminations: Bool[ArrayLike, " *size"],
59+
truncations: Bool[ArrayLike, " *size"],
60+
log_probs: Float[ArrayLike, " *size"],
61+
values: Float[ArrayLike, " *size"],
62+
states: PyTree[eqx.nn.State],
63+
returns: Float[ArrayLike, " *size"] | None = None,
64+
advantages: Float[ArrayLike, " *size"] | None = None,
65+
):
66+
"""
67+
Initialize the RolloutBuffer with the given parameters.
68+
69+
Returns and advantages can be provided, but if not, they will be filled with
70+
NaNs.
71+
"""
72+
# TODO: Add type checks for observations, actions, etc
73+
# TODO: Add shape checks for rewards, terminations, truncations, log_probs,
74+
# values
75+
76+
self.observations = observations
77+
self.actions = actions
78+
self.rewards = jnp.asarray(rewards)
79+
self.terminations = jnp.asarray(terminations)
80+
self.truncations = jnp.asarray(truncations)
81+
self.log_probs = jnp.asarray(log_probs)
82+
self.values = jnp.asarray(values)
83+
self.states = states
84+
self.returns = (
85+
jnp.asarray(returns) if returns else jnp.full_like(values, jnp.nan)
86+
)
87+
self.advantages = (
88+
jnp.asarray(advantages) if advantages else jnp.full_like(values, jnp.nan)
89+
)
90+
91+
@eqx.filter_jit
2592
def compute_returns_and_advantages(
26-
self, last_value: Array, dones: Array
27-
) -> RolloutBuffer:
28-
pass
93+
self,
94+
last_value: Float[ArrayLike, ""],
95+
done: Bool[ArrayLike, ""],
96+
gae_lambda: Float[ArrayLike, ""],
97+
gamma: Float[ArrayLike, ""],
98+
) -> RolloutBuffer[ActType, ObsType]:
99+
"""
100+
Compute returns and advantages for the rollout buffer using Generalized
101+
Advantage Estimation.
102+
103+
Works under JIT compilation.
104+
105+
:param last_value: The estimated value of the last state.
106+
:param done: Whether the last state is terminal.
107+
:param gae_lambda: The lambda parameter for GAE.
108+
:param gamma: The discount factor for future rewards.
109+
"""
110+
last_value = jnp.asarray(last_value)
111+
done = jnp.asarray(done)
112+
gamma = jnp.asarray(gamma)
113+
gae_lambda = jnp.asarray(gae_lambda)
114+
115+
dones = jnp.logical_or(self.terminations, self.truncations)
116+
117+
next_values = jnp.concatenate(
118+
[self.values[1:], jnp.array([last_value])], axis=0
119+
)
120+
121+
next_non_terminal = jnp.concatenate(
122+
[1.0 - dones, jnp.array([1.0 - done])], axis=0
123+
)
124+
125+
deltas = self.rewards + gamma * next_values * next_non_terminal - self.values
126+
127+
def scan_fn(
128+
advantage_carry: Float[Array, ""],
129+
x: tuple[Float[Array, ""], Float[Array, ""]],
130+
) -> tuple[Float[Array, ""], Float[Array, ""]]:
131+
delta, next_non_terminal = x
132+
advantage = delta + gamma * gae_lambda * next_non_terminal * advantage_carry
133+
return advantage, advantage
134+
135+
_, advantages = lax.scan(
136+
scan_fn, jnp.array(0.0), (jnp.flip(deltas), jnp.flip(next_non_terminal))
137+
)
138+
advantages = jnp.flip(advantages)
139+
returns = advantages + self.values
140+
141+
return dataclasses.replace(self, advantages=advantages, returns=returns)
142+
143+
def batches(
144+
self, batch_size: int, *, key: Key | None = None
145+
) -> RolloutBuffer[ActType, ObsType]:
146+
"""
147+
Return rollout buffer with batches of the given size.
148+
149+
The buffer is shuffled if a key is provided.
150+
151+
This method reshapes the buffer into batches of the specified size. It is not
152+
the same as a list of butches, but rather a single buffer where each batch is a
153+
slice of the original data.
154+
"""
155+
# TODO: Add warning if batch_size does not divide the number of samples
156+
if key is None:
157+
indices = jnp.arange(self.shape[0])
158+
else:
159+
indices = jr.permutation(key, self.shape[0])
160+
indices = indices.reshape(-1, batch_size)
161+
162+
return jax.tree.map(partial(jnp.take, indices=indices, axis=0), self)
163+
164+
@property
165+
def shape(self) -> tuple[int, ...]:
166+
return self.rewards.shape

0 commit comments

Comments
 (0)