pub fn compute_advantages(
buffer: &mut super::storage::RolloutBuffer,
last_values: &[f32],
gamma: f32,
gae_lambda: f32,
) {
let num_steps = buffer.shape().0;
compute_advantages_partial(buffer, num_steps, last_values, gamma, gae_lambda);
}
pub fn compute_advantages_partial(
buffer: &mut super::storage::RolloutBuffer,
valid_steps: usize,
last_values: &[f32],
gamma: f32,
gae_lambda: f32,
) {
let (num_steps, num_envs) = (buffer.shape().0, buffer.shape().1);
assert!(
valid_steps <= num_steps,
"valid_steps ({}) must not exceed buffer.num_steps ({})",
valid_steps,
num_steps
);
debug_assert_eq!(last_values.len(), num_envs, "last_values length mismatch");
if valid_steps == 0 {
return;
}
let rewards: Vec<Vec<f32>> = buffer.rewards().iter().map(|step| step.to_vec()).collect();
let values: Vec<Vec<f32>> = buffer.values().iter().map(|step| step.to_vec()).collect();
let terminated: Vec<Vec<bool>> = buffer.terminated().iter().map(|step| step.to_vec()).collect();
let (advantages, returns) = buffer.advantages_and_returns_mut();
for env_id in 0..num_envs {
let env_rewards: Vec<f32> =
rewards.iter().take(valid_steps).map(|step| step[env_id]).collect();
let env_values: Vec<f32> =
values.iter().take(valid_steps).map(|step| step[env_id]).collect();
let env_terminated: Vec<bool> =
terminated.iter().take(valid_steps).map(|step| step[env_id]).collect();
let mut env_advantages: Vec<f32> = vec![0.0; valid_steps];
let mut env_returns: Vec<f32> = vec![0.0; valid_steps];
compute_gae_single_env(
&env_rewards,
&env_values,
&env_terminated,
last_values[env_id],
gamma,
gae_lambda,
&mut env_advantages,
&mut env_returns,
);
for step in 0..valid_steps {
advantages[step][env_id] = env_advantages[step];
returns[step][env_id] = env_returns[step];
}
}
}
pub(super) fn compute_gae_single_env(
rewards: &[f32],
values: &[f32],
terminated: &[bool],
last_value: f32,
gamma: f32,
gae_lambda: f32,
advantages: &mut [f32],
returns: &mut [f32],
) {
let num_steps = rewards.len();
debug_assert_eq!(values.len(), num_steps);
debug_assert_eq!(terminated.len(), num_steps);
debug_assert_eq!(advantages.len(), num_steps);
debug_assert_eq!(returns.len(), num_steps);
let mut gae = 0.0;
for t in (0..num_steps).rev() {
if terminated[t] {
gae = 0.0;
}
let next_value = if t == num_steps - 1 {
last_value
} else if terminated[t] {
0.0
} else {
values[t + 1]
};
let delta = rewards[t] + gamma * next_value - values[t];
gae = delta + gamma * gae_lambda * gae;
advantages[t] = gae;
returns[t] = values[t] + gae;
}
}
pub fn compute_advantages_multi_agent(
rewards: &[f32],
values: &[f32],
dones: &[bool],
last_values: &[f32],
num_envs: usize,
num_agents: usize,
gamma: f32,
gae_lambda: f32,
) -> (Vec<f32>, Vec<f32>) {
let stride = num_envs * num_agents;
assert!(stride > 0, "num_envs * num_agents must be > 0");
assert_eq!(
rewards.len() % stride,
0,
"rewards.len() ({}) must be divisible by num_envs * num_agents ({})",
rewards.len(),
stride
);
assert_eq!(values.len(), rewards.len(), "values length mismatch");
assert_eq!(dones.len(), rewards.len(), "dones length mismatch");
assert_eq!(
last_values.len(),
stride,
"last_values length must equal num_envs * num_agents ({})",
stride
);
let num_steps = rewards.len() / stride;
let total = rewards.len();
let mut advantages = vec![0.0_f32; total];
let mut returns = vec![0.0_f32; total];
for env in 0..num_envs {
for agent in 0..num_agents {
let slot = env * num_agents + agent;
let bootstrap = last_values[slot];
let mut gae = 0.0_f32;
for t in (0..num_steps).rev() {
let idx = t * stride + slot;
let done = dones[idx];
let next_value = if t == num_steps - 1 {
if done { 0.0 } else { bootstrap }
} else if done {
0.0
} else {
values[idx + stride]
};
let mask = if done { 0.0_f32 } else { 1.0_f32 };
let delta = rewards[idx] + gamma * next_value * mask - values[idx];
gae = delta + gamma * gae_lambda * mask * gae;
advantages[idx] = gae;
returns[idx] = values[idx] + gae;
}
}
}
(advantages, returns)
}
pub fn compute_nstep_returns(
buffer: &mut super::storage::RolloutBuffer,
last_values: &[f32],
gamma: f32,
) {
let (num_steps, num_envs) = (buffer.shape().0, buffer.shape().1);
debug_assert_eq!(last_values.len(), num_envs, "last_values length mismatch");
let rewards: Vec<Vec<f32>> = buffer.rewards().iter().map(|step| step.to_vec()).collect();
let terminated: Vec<Vec<bool>> = buffer.terminated().iter().map(|step| step.to_vec()).collect();
let returns = buffer.returns_mut();
for env_id in 0..num_envs {
let mut discounted_return = last_values[env_id];
for step in (0..num_steps).rev() {
if terminated[step][env_id] {
discounted_return = 0.0;
}
discounted_return = rewards[step][env_id] + gamma * discounted_return;
returns[step][env_id] = discounted_return;
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn compute_mc_returns(buffer: &mut super::storage::RolloutBuffer) {
let (num_steps, num_envs) = (buffer.shape().0, buffer.shape().1);
let rewards: Vec<Vec<f32>> = buffer.rewards().iter().map(|step| step.to_vec()).collect();
let terminated: Vec<Vec<bool>> = buffer.terminated().iter().map(|step| step.to_vec()).collect();
let returns = buffer.returns_mut();
for env_id in 0..num_envs {
let mut episode_return = 0.0;
let mut episode_start = 0;
for step in 0..num_steps {
episode_return += rewards[step][env_id];
if terminated[step][env_id] || step == num_steps - 1 {
for s in episode_start..=step {
returns[s][env_id] = episode_return;
}
episode_return = 0.0;
episode_start = step + 1;
}
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn normalize_advantages(buffer: &mut super::storage::RolloutBuffer) {
let (num_steps, num_envs) = (buffer.shape().0, buffer.shape().1);
let mut all_advantages = Vec::with_capacity(num_steps * num_envs);
for step in 0..num_steps {
for env in 0..num_envs {
all_advantages.push(buffer.advantages()[step][env]);
}
}
let mean: f32 = all_advantages.iter().sum::<f32>() / all_advantages.len() as f32;
let variance: f32 = all_advantages.iter().map(|&x| (x - mean).powi(2)).sum::<f32>()
/ all_advantages.len() as f32;
let std = variance.sqrt().max(1e-8);
let advantages = buffer.advantages_mut();
for step in 0..num_steps {
for env in 0..num_envs {
advantages[step][env] = (advantages[step][env] - mean) / std;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::rollout::storage::RolloutBuffer;
#[test]
fn test_compute_advantages_partial_matches_full_on_prefix() {
let valid_steps = 3usize;
let total_capacity = 8usize;
let num_envs = 1usize;
let obs_dim = 1usize;
let mut partial_buffer = RolloutBuffer::new(total_capacity, num_envs, obs_dim);
let rewards = [1.0_f32, 0.5, -0.25];
let values = [0.4_f32, 0.6, 0.8];
let log_probs = [-0.1_f32, -0.2, -0.3];
for step in 0..valid_steps {
partial_buffer.add(
step,
0,
&[0.0],
0,
rewards[step],
values[step],
log_probs[step],
false,
false,
);
}
let mut tight_buffer = RolloutBuffer::new(valid_steps, num_envs, obs_dim);
for step in 0..valid_steps {
tight_buffer.add(
step,
0,
&[0.0],
0,
rewards[step],
values[step],
log_probs[step],
false,
false,
);
}
let last_values = vec![0.7_f32];
let gamma = 0.99_f32;
let gae_lambda = 0.95_f32;
compute_advantages_partial(
&mut partial_buffer,
valid_steps,
&last_values,
gamma,
gae_lambda,
);
compute_advantages(&mut tight_buffer, &last_values, gamma, gae_lambda);
for step in 0..valid_steps {
let p_adv = partial_buffer.advantages()[step][0];
let t_adv = tight_buffer.advantages()[step][0];
let p_ret = partial_buffer.returns()[step][0];
let t_ret = tight_buffer.returns()[step][0];
assert!(
(p_adv - t_adv).abs() < 1e-6_f32,
"advantage mismatch at step {}: partial={}, tight={}",
step,
p_adv,
t_adv
);
assert!(
(p_ret - t_ret).abs() < 1e-6_f32,
"return mismatch at step {}: partial={}, tight={}",
step,
p_ret,
t_ret
);
}
for step in valid_steps..total_capacity {
assert_eq!(partial_buffer.advantages()[step][0], 0.0);
assert_eq!(partial_buffer.returns()[step][0], 0.0);
}
}
#[test]
fn test_compute_advantages_partial_does_not_leak_bootstrap_through_padding() {
let valid_steps = 2usize;
let total_capacity = 16usize;
let num_envs = 1usize;
let mut buffer = RolloutBuffer::new(total_capacity, num_envs, 1);
buffer.add(0, 0, &[0.0], 0, 0.0, 0.0, 0.0, false, false);
buffer.add(1, 0, &[0.0], 0, 0.0, 0.0, 0.0, false, false);
let bootstrap = 1.0_f32;
let last_values = vec![bootstrap];
let gamma = 0.99_f32;
let gae_lambda = 0.95_f32;
compute_advantages_partial(&mut buffer, valid_steps, &last_values, gamma, gae_lambda);
let expected_adv_1 = gamma * bootstrap;
let expected_adv_0 = gamma * gamma * gae_lambda * bootstrap;
let a1 = buffer.advantages()[1][0];
let a0 = buffer.advantages()[0][0];
assert!(
(a1 - expected_adv_1).abs() < 1e-6_f32,
"advantage[1] expected {}, got {}",
expected_adv_1,
a1
);
assert!(
(a0 - expected_adv_0).abs() < 1e-6_f32,
"advantage[0] expected {} (γ²λ * bootstrap), got {}. \
If this is closer to γ^15 * bootstrap, the GAE iteration is \
still walking through padded rows.",
expected_adv_0,
a0
);
for step in valid_steps..total_capacity {
assert_eq!(buffer.advantages()[step][0], 0.0);
}
}
#[test]
fn test_compute_advantages_partial_zero_valid_is_noop() {
let mut buffer = RolloutBuffer::new(4, 1, 1);
buffer.add(0, 0, &[0.0], 0, 1.0, 0.5, 0.0, false, false);
buffer.advantages_mut()[0][0] = 99.0;
compute_advantages_partial(&mut buffer, 0, &[0.0], 0.99, 0.95);
assert_eq!(buffer.advantages()[0][0], 99.0);
}
#[test]
#[should_panic(expected = "valid_steps")]
fn test_compute_advantages_partial_panics_on_overflow() {
let mut buffer = RolloutBuffer::new(4, 1, 1);
compute_advantages_partial(&mut buffer, 5, &[0.0], 0.99, 0.95);
}
}