use burn::tensor::{Int, Tensor as BurnTensor, TensorData, backend::Backend};
#[derive(Debug, Clone)]
pub struct RolloutBuffer {
num_steps: usize,
num_envs: usize,
obs_dim: usize,
observations: Vec<Vec<Vec<f32>>>,
actions: Vec<Vec<i64>>,
rewards: Vec<Vec<f32>>,
values: Vec<Vec<f32>>,
log_probs: Vec<Vec<f32>>,
terminated: Vec<Vec<bool>>,
truncated: Vec<Vec<bool>>,
advantages: Vec<Vec<f32>>,
returns: Vec<Vec<f32>>,
}
impl RolloutBuffer {
pub fn new(num_steps: usize, num_envs: usize, obs_dim: usize) -> Self {
let observations = vec![vec![vec![0.0; obs_dim]; num_envs]; num_steps];
let actions = vec![vec![0; num_envs]; num_steps];
let rewards = vec![vec![0.0; num_envs]; num_steps];
let values = vec![vec![0.0; num_envs]; num_steps];
let log_probs = vec![vec![0.0; num_envs]; num_steps];
let terminated = vec![vec![false; num_envs]; num_steps];
let truncated = vec![vec![false; num_envs]; num_steps];
let advantages = vec![vec![0.0; num_envs]; num_steps];
let returns = vec![vec![0.0; num_envs]; num_steps];
Self {
num_steps,
num_envs,
obs_dim,
observations,
actions,
rewards,
values,
log_probs,
terminated,
truncated,
advantages,
returns,
}
}
#[allow(clippy::too_many_arguments)]
pub fn add(
&mut self,
step: usize,
env_id: usize,
observation: &[f32],
action: i64,
reward: f32,
value: f32,
log_prob: f32,
terminated: bool,
truncated: bool,
) {
debug_assert!(step < self.num_steps, "step {} >= num_steps {}", step, self.num_steps);
debug_assert!(env_id < self.num_envs, "env_id {} >= num_envs {}", env_id, self.num_envs);
debug_assert_eq!(observation.len(), self.obs_dim, "observation dimension mismatch");
self.observations[step][env_id].copy_from_slice(observation);
self.actions[step][env_id] = action;
self.rewards[step][env_id] = reward;
self.values[step][env_id] = value;
self.log_probs[step][env_id] = log_prob;
self.terminated[step][env_id] = terminated;
self.truncated[step][env_id] = truncated;
}
pub fn reset(&mut self) {
for step in 0..self.num_steps {
for env in 0..self.num_envs {
self.advantages[step][env] = 0.0;
self.returns[step][env] = 0.0;
}
}
}
pub fn shape(&self) -> (usize, usize, usize) {
(self.num_steps, self.num_envs, self.obs_dim)
}
pub fn len(&self) -> usize {
self.num_steps * self.num_envs
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn obs_shape(&self) -> (usize, usize) {
(self.num_steps * self.num_envs, self.obs_dim)
}
pub fn observations(&self) -> &[Vec<Vec<f32>>] {
&self.observations
}
pub fn actions(&self) -> &[Vec<i64>] {
&self.actions
}
pub fn rewards(&self) -> &[Vec<f32>] {
&self.rewards
}
pub fn values(&self) -> &[Vec<f32>] {
&self.values
}
pub fn log_probs(&self) -> &[Vec<f32>] {
&self.log_probs
}
pub fn terminated(&self) -> &[Vec<bool>] {
&self.terminated
}
pub fn truncated(&self) -> &[Vec<bool>] {
&self.truncated
}
pub fn advantages(&self) -> &[Vec<f32>] {
&self.advantages
}
pub fn returns(&self) -> &[Vec<f32>] {
&self.returns
}
pub fn advantages_mut(&mut self) -> &mut [Vec<f32>] {
&mut self.advantages
}
pub fn returns_mut(&mut self) -> &mut [Vec<f32>] {
&mut self.returns
}
pub fn advantages_and_returns_mut(&mut self) -> (&mut [Vec<f32>], &mut [Vec<f32>]) {
(&mut self.advantages, &mut self.returns)
}
}
#[derive(Debug, Clone)]
pub struct RolloutBatch {
pub observations: Vec<f32>,
pub actions: Vec<i64>,
pub old_log_probs: Vec<f32>,
pub old_values: Vec<f32>,
pub advantages: Vec<f32>,
pub returns: Vec<f32>,
}
impl RolloutBatch {
pub fn from_buffer(buffer: &RolloutBuffer) -> Self {
Self::from_buffer_partial(buffer, buffer.num_steps)
}
pub fn from_buffer_partial(buffer: &RolloutBuffer, valid_steps: usize) -> Self {
assert!(
valid_steps <= buffer.num_steps,
"valid_steps ({}) must not exceed buffer.num_steps ({})",
valid_steps,
buffer.num_steps
);
let batch_size = valid_steps * buffer.num_envs;
let obs_size = batch_size * buffer.obs_dim;
let mut observations = Vec::with_capacity(obs_size);
let mut actions = Vec::with_capacity(batch_size);
let mut old_log_probs = Vec::with_capacity(batch_size);
let mut old_values = Vec::with_capacity(batch_size);
let mut advantages = Vec::with_capacity(batch_size);
let mut returns = Vec::with_capacity(batch_size);
for step in 0..valid_steps {
for env in 0..buffer.num_envs {
observations.extend_from_slice(&buffer.observations[step][env]);
actions.push(buffer.actions[step][env]);
old_log_probs.push(buffer.log_probs[step][env]);
old_values.push(buffer.values[step][env]);
advantages.push(buffer.advantages[step][env]);
returns.push(buffer.returns[step][env]);
}
}
Self { observations, actions, old_log_probs, old_values, advantages, returns }
}
pub fn len(&self) -> usize {
self.actions.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn obs_shape(&self) -> (usize, usize) {
let batch_size = self.len();
let obs_dim = self.observations.len().checked_div(batch_size).unwrap_or(0);
(batch_size, obs_dim)
}
pub fn to_burn_tensors<B: Backend>(&self, device: &B::Device) -> RolloutBurnTensors<B> {
let (batch_size, obs_dim) = self.obs_shape();
let observations = BurnTensor::<B, 2>::from_data(
TensorData::new(self.observations.clone(), [batch_size, obs_dim]),
device,
);
let actions = BurnTensor::<B, 1, Int>::from_data(
TensorData::new(self.actions.clone(), [batch_size]),
device,
);
let old_log_probs = BurnTensor::<B, 1>::from_data(
TensorData::new(self.old_log_probs.clone(), [batch_size]),
device,
);
let old_values = BurnTensor::<B, 1>::from_data(
TensorData::new(self.old_values.clone(), [batch_size]),
device,
);
let advantages = BurnTensor::<B, 1>::from_data(
TensorData::new(self.advantages.clone(), [batch_size]),
device,
);
let returns = BurnTensor::<B, 1>::from_data(
TensorData::new(self.returns.clone(), [batch_size]),
device,
);
RolloutBurnTensors { observations, actions, old_log_probs, old_values, advantages, returns }
}
}
#[derive(Debug)]
pub struct RolloutBurnTensors<B: Backend> {
pub observations: BurnTensor<B, 2>,
pub actions: BurnTensor<B, 1, Int>,
pub old_log_probs: BurnTensor<B, 1>,
pub old_values: BurnTensor<B, 1>,
pub advantages: BurnTensor<B, 1>,
pub returns: BurnTensor<B, 1>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rollout_buffer_creation() {
let buffer = RolloutBuffer::new(10, 2, 4);
assert_eq!(buffer.shape(), (10, 2, 4));
assert_eq!(buffer.len(), 20); assert!(!buffer.is_empty());
}
#[test]
fn test_rollout_buffer_add_and_reset() {
let mut buffer = RolloutBuffer::new(5, 1, 2);
buffer.add(0, 0, &[1.0, 2.0], 1, 1.5, 0.8, -0.2, false, false);
buffer.add(1, 0, &[2.0, 3.0], 0, 2.0, 1.2, -0.1, false, false);
assert_eq!(buffer.actions()[0][0], 1);
assert_eq!(buffer.rewards()[0][0], 1.5);
assert_eq!(buffer.observations()[0][0], vec![1.0, 2.0]);
buffer.reset();
assert_eq!(buffer.advantages()[0][0], 0.0);
assert_eq!(buffer.returns()[0][0], 0.0);
}
#[test]
fn test_rollout_batch_from_buffer() {
let mut buffer = RolloutBuffer::new(2, 1, 2);
buffer.add(0, 0, &[1.0, 2.0], 1, 1.5, 0.8, -0.2, false, false);
buffer.add(1, 0, &[2.0, 3.0], 0, 2.0, 1.2, -0.1, false, false);
buffer.advantages_mut()[0][0] = 0.5;
buffer.returns_mut()[0][0] = 1.3;
buffer.advantages_mut()[1][0] = 0.8;
buffer.returns_mut()[1][0] = 2.0;
let batch = RolloutBatch::from_buffer(&buffer);
assert_eq!(batch.len(), 2);
assert_eq!(batch.actions, vec![1, 0]);
assert_eq!(batch.advantages, vec![0.5, 0.8]);
assert_eq!(batch.returns, vec![1.3, 2.0]);
assert_eq!(batch.observations, vec![1.0, 2.0, 2.0, 3.0]);
}
#[test]
fn test_rollout_batch_from_buffer_partial_skips_unfilled_tail() {
let mut buffer = RolloutBuffer::new(4, 1, 2);
buffer.add(0, 0, &[1.0, 2.0], 1, 1.5, 0.8, -0.2, false, false);
buffer.add(1, 0, &[2.0, 3.0], 0, 2.0, 1.2, -0.1, false, false);
buffer.advantages_mut()[0][0] = 0.5;
buffer.returns_mut()[0][0] = 1.3;
buffer.advantages_mut()[1][0] = 0.8;
buffer.returns_mut()[1][0] = 2.0;
let batch = RolloutBatch::from_buffer_partial(&buffer, 2);
assert_eq!(batch.len(), 2);
assert_eq!(batch.actions, vec![1, 0]);
assert_eq!(batch.old_values, vec![0.8, 1.2]);
assert_eq!(batch.old_log_probs, vec![-0.2, -0.1]);
assert_eq!(batch.advantages, vec![0.5, 0.8]);
assert_eq!(batch.returns, vec![1.3, 2.0]);
assert_eq!(batch.observations, vec![1.0, 2.0, 2.0, 3.0]);
let full_batch = RolloutBatch::from_buffer(&buffer);
assert_eq!(full_batch.len(), 4);
}
#[test]
fn test_rollout_batch_from_buffer_partial_zero_valid() {
let buffer = RolloutBuffer::new(4, 2, 3);
let batch = RolloutBatch::from_buffer_partial(&buffer, 0);
assert!(batch.is_empty());
assert_eq!(batch.actions.len(), 0);
assert_eq!(batch.observations.len(), 0);
}
#[test]
#[should_panic(expected = "valid_steps")]
fn test_rollout_batch_from_buffer_partial_panics_on_overflow() {
let buffer = RolloutBuffer::new(4, 1, 2);
let _ = RolloutBatch::from_buffer_partial(&buffer, 5);
}
mod burn_tests {
use burn::backend::NdArray;
use super::*;
type B = NdArray<f32>;
#[test]
fn test_to_burn_tensors_matches_inline_construction() {
let batch = RolloutBatch {
observations: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
actions: vec![0, 1, 0],
old_log_probs: vec![-0.1, -0.2, -0.3],
old_values: vec![0.5, 0.7, 0.9],
advantages: vec![0.1, -0.2, 0.3],
returns: vec![0.6, 0.5, 1.2],
};
let device = crate::utils::cuda::default_burn_device::<B>();
let t = batch.to_burn_tensors::<B>(&device);
assert_eq!(t.observations.dims(), [3, 2]);
assert_eq!(t.actions.dims(), [3]);
assert_eq!(t.old_log_probs.dims(), [3]);
assert_eq!(t.old_values.dims(), [3]);
assert_eq!(t.advantages.dims(), [3]);
assert_eq!(t.returns.dims(), [3]);
let obs_flat: Vec<f32> = t.observations.into_data().to_vec().unwrap();
assert_eq!(obs_flat, batch.observations);
let acts: Vec<i64> = t.actions.into_data().to_vec().unwrap();
assert_eq!(acts, batch.actions);
let lp: Vec<f32> = t.old_log_probs.into_data().to_vec().unwrap();
assert_eq!(lp, batch.old_log_probs);
let v: Vec<f32> = t.old_values.into_data().to_vec().unwrap();
assert_eq!(v, batch.old_values);
let adv: Vec<f32> = t.advantages.into_data().to_vec().unwrap();
assert_eq!(adv, batch.advantages);
let ret: Vec<f32> = t.returns.into_data().to_vec().unwrap();
assert_eq!(ret, batch.returns);
}
#[test]
fn test_to_burn_tensors_empty_batch() {
let batch = RolloutBatch {
observations: vec![],
actions: vec![],
old_log_probs: vec![],
old_values: vec![],
advantages: vec![],
returns: vec![],
};
let device = crate::utils::cuda::default_burn_device::<B>();
let t = batch.to_burn_tensors::<B>(&device);
assert_eq!(t.observations.dims(), [0, 0]);
assert_eq!(t.actions.dims(), [0]);
assert_eq!(t.old_log_probs.dims(), [0]);
assert_eq!(t.old_values.dims(), [0]);
assert_eq!(t.advantages.dims(), [0]);
assert_eq!(t.returns.dims(), [0]);
}
}
#[test]
fn test_rollout_batch_properties() {
let batch = RolloutBatch {
observations: vec![1.0, 2.0, 3.0, 4.0],
actions: vec![0, 1],
old_log_probs: vec![-0.1, -0.2],
old_values: vec![0.5, 0.8],
advantages: vec![0.3, 0.6],
returns: vec![1.0, 1.5],
};
assert_eq!(batch.len(), 2);
assert_eq!(batch.obs_shape(), (2, 2)); assert!(!batch.is_empty());
}
}