use crate::{Engine, Error, Matrix, Result};
use super::{ItTraining, ItTrainingConfig, Optimizer, TrainingSnapshot};
use crate::ml::{RolloutBuffer, advantage::GaeConfig};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ItRolloutTrainingConfig {
pub rollouts: u64,
pub horizon: usize,
pub environments: usize,
pub update_epochs: u64,
pub timer_name: String,
pub enable_gpu_timing: bool,
}
impl Default for ItRolloutTrainingConfig {
fn default() -> Self {
Self {
rollouts: 0,
horizon: 0,
environments: 0,
update_epochs: 0,
timer_name: "rl_update".into(),
enable_gpu_timing: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RolloutTrainingPhase {
Collect,
Update,
Complete,
}
#[must_use]
pub struct ItRolloutTraining<'engine, 'hooks> {
config: ItRolloutTrainingConfig,
updates: ItTraining<'engine, 'hooks>,
phase: RolloutTrainingPhase,
rollout_index: u64,
update_epoch: u64,
rollout_open: bool,
update_body_pending: bool,
active_rollout: Option<*const RolloutBuffer>,
}
impl<'engine, 'hooks> ItRolloutTraining<'engine, 'hooks> {
pub fn new(
engine: &'engine Engine,
optimizer: &'engine mut dyn Optimizer,
config: ItRolloutTrainingConfig,
) -> Result<Self> {
if config.rollouts == 0
|| config.horizon == 0
|| config.environments == 0
|| config.update_epochs == 0
{
return Err(Error::invalid_argument(
"rollout training requires nonzero rollouts, horizon, environments, and update epochs",
));
}
let batch_size = config
.horizon
.checked_mul(config.environments)
.ok_or_else(|| Error::resource_exhausted("rollout training batch size overflows"))?;
let batch_size = u64::try_from(batch_size)
.map_err(|_| Error::resource_exhausted("rollout training batch exceeds u64"))?;
let total_steps = config
.rollouts
.checked_mul(config.update_epochs)
.ok_or_else(|| Error::resource_exhausted("rollout training update count overflows"))?;
let updates = ItTraining::new_eager(
engine,
optimizer,
ItTrainingConfig {
total_steps,
steps_per_epoch: config.update_epochs,
batch_size,
timer_name: config.timer_name.clone(),
enable_gpu_timing: config.enable_gpu_timing,
..ItTrainingConfig::default()
},
)?;
Ok(Self {
config,
updates,
phase: RolloutTrainingPhase::Collect,
rollout_index: 0,
update_epoch: 0,
rollout_open: false,
update_body_pending: false,
active_rollout: None,
})
}
pub fn begin_rollout(&mut self, rollout: &mut RolloutBuffer) -> Result<()> {
if self.phase != RolloutTrainingPhase::Collect || self.rollout_open || self.update_body_pending
{
return Err(Error::failed_precondition(
"begin_rollout requires an idle Collect phase",
));
}
if rollout.config().time != self.config.horizon
|| rollout.config().environments != self.config.environments
{
return Err(Error::invalid_argument(
"rollout buffer geometry does not match training configuration",
));
}
rollout.reset()?;
self.rollout_open = true;
self.active_rollout = Some(std::ptr::from_ref(rollout));
Ok(())
}
pub fn finalize_rollout(&mut self, rollout: &mut RolloutBuffer, config: GaeConfig) -> Result<()> {
if self.phase != RolloutTrainingPhase::Collect
|| !self.rollout_open
|| self.update_body_pending
|| !self.active_is(rollout)
{
return Err(Error::failed_precondition(
"finalize_rollout requires the active Collect buffer",
));
}
rollout.finalize(config)?;
self.rollout_open = false;
self.phase = RolloutTrainingPhase::Update;
self.update_epoch = 0;
Ok(())
}
pub fn abort_rollout(&mut self, rollout: &mut RolloutBuffer) -> Result<()> {
let open = self.phase == RolloutTrainingPhase::Collect && self.rollout_open;
let finalized =
self.phase == RolloutTrainingPhase::Update && !self.rollout_open && self.update_epoch == 0;
if (!open && !finalized) || self.update_body_pending || !self.active_is(rollout) {
return Err(Error::failed_precondition(
"abort_rollout requires an unsubmitted collection before its first update",
));
}
rollout.abort_unsubmitted();
self.rollout_open = false;
self.update_body_pending = false;
self.active_rollout = None;
self.phase = RolloutTrainingPhase::Collect;
self.update_epoch = 0;
Ok(())
}
pub fn begin_update(&mut self) -> Result<bool> {
if self.phase != RolloutTrainingPhase::Update
|| self.update_body_pending
|| self.update_epoch >= self.config.update_epochs
{
return Err(Error::failed_precondition(
"begin_update requires an idle Update phase",
));
}
if !self.updates.begin_step()? {
if self.updates.stop_requested() {
self.phase = RolloutTrainingPhase::Complete;
}
return Ok(false);
}
self.update_body_pending = true;
self.active_rollout = None;
Ok(true)
}
pub fn complete_update(&mut self, loss: &Matrix) -> Result<()> {
if self.phase != RolloutTrainingPhase::Update || !self.update_body_pending {
return Err(Error::failed_precondition(
"complete_update requires a preceding begin_update",
));
}
if let Err(error) = self.updates.complete_step(loss) {
self.update_body_pending = false;
self.phase = RolloutTrainingPhase::Complete;
return Err(error);
}
self.update_body_pending = false;
self.update_epoch += 1;
if self.update_epoch == self.config.update_epochs {
self.rollout_index += 1;
self.phase = if self.rollout_index == self.config.rollouts {
RolloutTrainingPhase::Complete
} else {
RolloutTrainingPhase::Collect
};
}
Ok(())
}
pub(super) fn fail_update<T>(&mut self, error: Error) -> Result<T> {
if !self.update_body_pending {
return Err(error);
}
self.update_body_pending = false;
self.phase = RolloutTrainingPhase::Complete;
self.updates.fail_composed_step(error)
}
pub fn is_done(&self) -> bool {
self.phase == RolloutTrainingPhase::Complete || self.updates.stop_requested()
}
pub const fn phase(&self) -> RolloutTrainingPhase {
self.phase
}
pub const fn rollout_index(&self) -> u64 {
self.rollout_index
}
pub const fn update_epoch(&self) -> u64 {
self.update_epoch
}
pub const fn config(&self) -> &ItRolloutTrainingConfig {
&self.config
}
pub const fn update_loop(&self) -> &ItTraining<'engine, 'hooks> {
&self.updates
}
pub fn update_loop_mut(&mut self) -> &mut ItTraining<'engine, 'hooks> {
&mut self.updates
}
pub fn finish(self) -> Result<TrainingSnapshot> {
if !self.is_done() || self.update_body_pending {
return Err(Error::failed_precondition(
"rollout training finish requires the Complete phase",
));
}
self.updates.finish()
}
fn active_is(&self, rollout: &RolloutBuffer) -> bool {
self
.active_rollout
.is_some_and(|active| std::ptr::eq(active, std::ptr::from_ref(rollout)))
}
}