use super::storage::RolloutBuffer;
#[allow(clippy::too_many_arguments)]
pub(super) fn compute_vtrace_single_env(
rewards: &[f32],
values: &[f32],
behavior_log_probs: &[f32],
target_log_probs: &[f32],
terminated: &[bool],
bootstrap_value: f32,
gamma: f32,
rho_bar: f32,
c_bar: f32,
vtrace_targets: &mut [f32],
advantages: &mut [f32],
) {
let num_steps = rewards.len();
debug_assert_eq!(values.len(), num_steps);
debug_assert_eq!(behavior_log_probs.len(), num_steps);
debug_assert_eq!(target_log_probs.len(), num_steps);
debug_assert_eq!(terminated.len(), num_steps);
debug_assert_eq!(vtrace_targets.len(), num_steps);
debug_assert_eq!(advantages.len(), num_steps);
for t in (0..num_steps).rev() {
let is_last = t == num_steps - 1;
let terminal = terminated[t];
let next_value = if is_last {
bootstrap_value
} else if terminal {
0.0
} else {
values[t + 1]
};
let next_vtrace = if is_last {
bootstrap_value
} else if terminal {
0.0
} else {
vtrace_targets[t + 1]
};
let next_v_minus_baseline = if is_last || terminal {
0.0
} else {
vtrace_targets[t + 1] - values[t + 1]
};
let ratio = (target_log_probs[t] - behavior_log_probs[t]).exp();
let rho = ratio.min(rho_bar);
let c = ratio.min(c_bar);
let delta = rho * (rewards[t] + gamma * next_value - values[t]);
let d = delta + gamma * c * next_v_minus_baseline;
vtrace_targets[t] = values[t] + d;
advantages[t] = rho * (rewards[t] + gamma * next_vtrace - values[t]);
}
}
pub fn compute_vtrace_advantages(
buffer: &mut RolloutBuffer,
target_log_probs: &[Vec<f32>],
last_values: &[f32],
gamma: f32,
rho_bar: f32,
c_bar: f32,
) {
let (num_steps, num_envs, _) = buffer.shape();
assert_eq!(
target_log_probs.len(),
num_steps,
"target_log_probs must have num_steps ({}) rows, got {}",
num_steps,
target_log_probs.len()
);
debug_assert_eq!(last_values.len(), num_envs, "last_values length mismatch");
if num_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 behavior_log_probs: Vec<Vec<f32>> =
buffer.log_probs().iter().map(|step| step.to_vec()).collect();
let terminated: Vec<Vec<bool>> = buffer.terminated().iter().map(|step| step.to_vec()).collect();
for (row, tlp) in target_log_probs.iter().enumerate() {
assert_eq!(
tlp.len(),
num_envs,
"target_log_probs row {} must have num_envs ({}) columns, got {}",
row,
num_envs,
tlp.len()
);
}
let (advantages, returns) = buffer.advantages_and_returns_mut();
for env_id in 0..num_envs {
let env_rewards: Vec<f32> = rewards.iter().map(|step| step[env_id]).collect();
let env_values: Vec<f32> = values.iter().map(|step| step[env_id]).collect();
let env_behavior: Vec<f32> = behavior_log_probs.iter().map(|step| step[env_id]).collect();
let env_target: Vec<f32> = target_log_probs.iter().map(|step| step[env_id]).collect();
let env_terminated: Vec<bool> = terminated.iter().map(|step| step[env_id]).collect();
let mut env_targets: Vec<f32> = vec![0.0; num_steps];
let mut env_advantages: Vec<f32> = vec![0.0; num_steps];
compute_vtrace_single_env(
&env_rewards,
&env_values,
&env_behavior,
&env_target,
&env_terminated,
last_values[env_id],
gamma,
rho_bar,
c_bar,
&mut env_targets,
&mut env_advantages,
);
for step in 0..num_steps {
advantages[step][env_id] = env_advantages[step];
returns[step][env_id] = env_targets[step];
}
}
}
#[cfg(test)]
#[allow(clippy::excessive_precision)]
mod tests {
use super::*;
use crate::buffer::rollout::{gae::compute_gae_single_env, storage::RolloutBuffer};
const TOL: f32 = 1e-5;
fn assert_slice_close(got: &[f32], expected: &[f32], what: &str) {
assert_eq!(got.len(), expected.len(), "{what}: length mismatch");
for (i, (&g, &e)) in got.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < TOL, "{what}: mismatch at index {i}: got {g}, expected {e}");
}
}
#[test]
fn clipped_rho_matches_reference() {
let rewards = [1.0_f32, 0.0, -1.0, 2.0];
let values = [0.5_f32, 0.6, 0.7, 0.8];
let behavior = [-0.5_f32, -1.0, -0.7, -0.2];
let target = [-0.2_f32, -1.5, -0.4, -0.6];
let terminated = [false, false, false, false];
let mut vt = [0.0_f32; 4];
let mut adv = [0.0_f32; 4];
compute_vtrace_single_env(
&rewards,
&values,
&behavior,
&target,
&terminated,
0.9,
0.99,
1.0,
1.0,
&mut vt,
&mut adv,
);
let expected_vt = [1.9349601974_f32, 0.9444042398, 1.1796228241, 2.2016392163];
let expected_adv = [1.4349601974_f32, 0.3444042398, 0.4796228241, 1.4016392163];
assert_slice_close(&vt, &expected_vt, "vtrace_targets");
assert_slice_close(&adv, &expected_adv, "advantages");
}
#[test]
fn independent_rho_c_clipping_matches_reference() {
let rewards = [1.0_f32, 0.0, -1.0, 2.0];
let values = [0.5_f32, 0.6, 0.7, 0.8];
let behavior = [-0.5_f32, -1.0, -0.7, -0.2];
let target = [-0.2_f32, -1.5, -0.4, -0.6];
let terminated = [false, false, false, false];
let mut vt = [0.0_f32; 4];
let mut adv = [0.0_f32; 4];
compute_vtrace_single_env(
&rewards,
&values,
&behavior,
&target,
&terminated,
0.9,
0.99,
0.5, 1.5, &mut vt,
&mut adv,
);
let expected_vt = [1.8659718836_f32, 1.2128376703, 1.6431646095, 1.8455000000];
let expected_adv = [0.8503546468_f32, 0.5133664817, 0.0635225000, 1.0455000000];
assert_slice_close(&vt, &expected_vt, "vtrace_targets");
assert_slice_close(&adv, &expected_adv, "advantages");
let mut vt_b = [0.0_f32; 4];
let mut adv_b = [0.0_f32; 4];
compute_vtrace_single_env(
&rewards,
&values,
&behavior,
&target,
&terminated,
0.9,
0.99,
1.0,
1.0,
&mut vt_b,
&mut adv_b,
);
assert!(
(vt_b[0] - vt[0]).abs() > 1e-3,
"different (rho_bar, c_bar) must yield different targets"
);
}
#[test]
fn on_policy_recovers_nstep_returns() {
let rewards = [1.0_f32, 0.0, -1.0, 2.0, 0.3];
let values = [0.5_f32, 0.6, 0.7, 0.8, 0.4];
let log_probs = [-0.5_f32, -1.0, -0.7, -0.2, -0.9];
let terminated = [false, false, false, false, false];
let bootstrap = 0.9_f32;
let gamma = 0.99_f32;
let mut vt = [0.0_f32; 5];
let mut adv = [0.0_f32; 5];
compute_vtrace_single_env(
&rewards,
&values,
&log_probs, &log_probs, &terminated,
bootstrap,
gamma,
1.0,
1.0,
&mut vt,
&mut adv,
);
let mut gae_adv = [0.0_f32; 5];
let mut gae_ret = [0.0_f32; 5];
compute_gae_single_env(
&rewards,
&values,
&terminated,
bootstrap,
gamma,
1.0, &mut gae_adv,
&mut gae_ret,
);
assert_slice_close(&vt, &gae_ret, "vtrace_targets vs n-step returns");
assert_slice_close(&adv, &gae_adv, "vtrace advantages vs GAE(lambda=1)");
}
#[test]
fn terminal_last_step_bootstraps_like_gae() {
let rewards = [1.0_f32, 0.5, -0.5, 2.0];
let values = [0.4_f32, 0.6, 0.8, 0.3];
let log_probs = [-0.5_f32, -1.0, -0.7, -0.2];
let terminated = [false, true, false, true];
let bootstrap = 0.9_f32;
let gamma = 0.99_f32;
let mut vt = [0.0_f32; 4];
let mut adv = [0.0_f32; 4];
compute_vtrace_single_env(
&rewards,
&values,
&log_probs,
&log_probs,
&terminated,
bootstrap,
gamma,
1.0,
1.0,
&mut vt,
&mut adv,
);
let mut gae_adv = [0.0_f32; 4];
let mut gae_ret = [0.0_f32; 4];
compute_gae_single_env(
&rewards,
&values,
&terminated,
bootstrap,
gamma,
1.0,
&mut gae_adv,
&mut gae_ret,
);
assert_slice_close(&vt, &gae_ret, "vtrace_targets vs GAE returns (terminal last step)");
assert_slice_close(&adv, &gae_adv, "vtrace advantages vs GAE (terminal last step)");
}
#[test]
fn episode_boundary_blocks_carryover() {
let rewards = [1.0_f32, 0.5, -0.5];
let values = [0.4_f32, 0.6, 0.8];
let behavior = [-0.5_f32, -1.0, -0.7];
let target = [-0.2_f32, -1.5, -0.4];
let terminated = [false, true, false];
let mut vt = [0.0_f32; 3];
let mut adv = [0.0_f32; 3];
compute_vtrace_single_env(
&rewards,
&values,
&behavior,
&target,
&terminated,
0.9,
0.99,
1.0,
1.0,
&mut vt,
&mut adv,
);
let expected_vt = [1.5339534647_f32, 0.5393469340, 0.3910000000];
let expected_adv = [1.1339534647_f32, -0.0606530660, -0.4090000000];
assert_slice_close(&vt, &expected_vt, "vtrace_targets");
assert_slice_close(&adv, &expected_adv, "advantages");
let rewards_perturbed = [1.0_f32, 0.5, 100.0];
let mut vt2 = [0.0_f32; 3];
let mut adv2 = [0.0_f32; 3];
compute_vtrace_single_env(
&rewards_perturbed,
&values,
&behavior,
&target,
&terminated,
0.9,
0.99,
1.0,
1.0,
&mut vt2,
&mut adv2,
);
assert!((vt2[0] - vt[0]).abs() < TOL, "step-0 target leaked across boundary");
assert!((vt2[1] - vt[1]).abs() < TOL, "step-1 target leaked across boundary");
assert!((adv2[0] - adv[0]).abs() < TOL, "step-0 advantage leaked across boundary");
}
#[test]
fn buffer_level_matches_single_env_kernel() {
let num_steps = 4;
let num_envs = 2;
let obs_dim = 1;
let mut buffer = RolloutBuffer::new(num_steps, num_envs, obs_dim);
let rewards = [[1.0_f32, 0.0, -1.0, 2.0], [0.2, 0.4, 0.1, -0.3]];
let values = [[0.5_f32, 0.6, 0.7, 0.8], [0.1, 0.2, 0.15, 0.05]];
let behavior = [[-0.5_f32, -1.0, -0.7, -0.2], [-0.3, -0.9, -0.6, -0.4]];
let target = [[-0.2_f32, -1.5, -0.4, -0.6], [-0.1, -1.1, -0.8, -0.2]];
let last_values = [0.9_f32, 0.25];
let gamma = 0.99_f32;
let (rho_bar, c_bar) = (1.0_f32, 1.0_f32);
for step in 0..num_steps {
for env in 0..num_envs {
buffer.add(
step,
env,
&[0.0],
0,
rewards[env][step],
values[env][step],
behavior[env][step], false,
false,
);
}
}
let target_lp: Vec<Vec<f32>> = (0..num_steps)
.map(|step| (0..num_envs).map(|env| target[env][step]).collect())
.collect();
compute_vtrace_advantages(&mut buffer, &target_lp, &last_values, gamma, rho_bar, c_bar);
for env in 0..num_envs {
let mut vt = [0.0_f32; 4];
let mut adv = [0.0_f32; 4];
compute_vtrace_single_env(
&rewards[env],
&values[env],
&behavior[env],
&target[env],
&[false; 4],
last_values[env],
gamma,
rho_bar,
c_bar,
&mut vt,
&mut adv,
);
for step in 0..num_steps {
assert!(
(buffer.advantages()[step][env] - adv[step]).abs() < TOL,
"advantage mismatch env {env} step {step}"
);
assert!(
(buffer.returns()[step][env] - vt[step]).abs() < TOL,
"return mismatch env {env} step {step}"
);
}
}
}
#[test]
fn buffer_on_policy_matches_gae_lambda_one() {
let num_steps = 5;
let num_envs = 2;
let mut buf_vtrace = RolloutBuffer::new(num_steps, num_envs, 1);
let mut buf_gae = RolloutBuffer::new(num_steps, num_envs, 1);
let rewards = [[1.0_f32, 0.5, 0.2, -0.3, 0.7], [0.3, -0.4, 0.6, 0.1, -0.2]];
let values = [[0.4_f32, 0.6, 0.8, 0.5, 0.35], [0.2, 0.1, 0.3, 0.4, 0.25]];
let log_probs = [[-0.5_f32, -1.0, -0.7, -0.4, -0.9], [-0.3, -0.9, -0.6, -0.8, -0.5]];
let terminated = [[false, false, true, false, false], [false, false, false, false, true]];
let last_values = [0.7_f32, 0.5];
let gamma = 0.99_f32;
for step in 0..num_steps {
for env in 0..num_envs {
let args = (rewards[env][step], values[env][step], log_probs[env][step]);
let term = terminated[env][step];
buf_vtrace.add(step, env, &[0.0], 0, args.0, args.1, args.2, term, false);
buf_gae.add(step, env, &[0.0], 0, args.0, args.1, args.2, term, false);
}
}
let target_lp: Vec<Vec<f32>> = (0..num_steps)
.map(|step| (0..num_envs).map(|env| log_probs[env][step]).collect())
.collect();
compute_vtrace_advantages(&mut buf_vtrace, &target_lp, &last_values, gamma, 1.0, 1.0);
crate::buffer::rollout::gae::compute_advantages(&mut buf_gae, &last_values, gamma, 1.0);
for step in 0..num_steps {
for env in 0..num_envs {
assert!(
(buf_vtrace.advantages()[step][env] - buf_gae.advantages()[step][env]).abs()
< TOL,
"advantage mismatch step {step} env {env}"
);
assert!(
(buf_vtrace.returns()[step][env] - buf_gae.returns()[step][env]).abs() < TOL,
"return mismatch step {step} env {env}"
);
}
}
}
#[test]
#[should_panic(expected = "target_log_probs")]
fn wrong_target_log_probs_shape_panics() {
let mut buffer = RolloutBuffer::new(4, 1, 1);
let target_lp = vec![vec![0.0_f32], vec![0.0_f32]];
compute_vtrace_advantages(&mut buffer, &target_lp, &[0.0], 0.99, 1.0, 1.0);
}
}