1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::thread::{self, JoinHandle};
4use std::time::Duration;
5
6use crossbeam_channel::{SendTimeoutError, Sender};
7
8pub type ValueFn = Arc<dyn Fn(&[f32]) -> Vec<f64> + Send + Sync>;
10
11pub type ActionFn = Arc<dyn Fn(&[f32]) -> (Vec<f32>, Vec<f64>) + Send + Sync>;
13
14const SEND_POLL_INTERVAL: Duration = Duration::from_millis(50);
18
19use crate::env::batch::BatchSteppable;
20use crate::env::spaces::Action;
21use crate::pipeline::channel::RolloutBatch;
22use crate::training::gae;
23
24pub struct AsyncCollector {
37 handle: Option<JoinHandle<()>>,
38 stop_flag: Arc<AtomicBool>,
39}
40
41impl AsyncCollector {
42 pub fn start(
51 mut envs: Box<dyn BatchSteppable>,
52 n_steps: usize,
53 gamma: f64,
54 gae_lambda: f64,
55 tx: Sender<RolloutBatch>,
56 value_fn: ValueFn,
57 action_fn: ActionFn,
58 ) -> Self {
59 let stop_flag = Arc::new(AtomicBool::new(false));
60 let stop = stop_flag.clone();
61
62 let handle = thread::spawn(move || {
63 let n_envs = envs.num_envs();
64
65 let obs_dim = match envs.obs_space() {
67 crate::env::spaces::ObsSpace::Discrete(_) => 1,
68 crate::env::spaces::ObsSpace::Box { shape, .. } => shape.iter().product(),
69 crate::env::spaces::ObsSpace::MultiDiscrete(v) => v.len(),
70 crate::env::spaces::ObsSpace::Dict(entries) => entries.iter().map(|(_, d)| d).sum(),
71 };
72
73 let act_dim = match envs.action_space() {
75 crate::env::spaces::ActionSpace::Discrete(_) => 1,
76 crate::env::spaces::ActionSpace::Box { shape, .. } => shape.iter().product(),
77 crate::env::spaces::ActionSpace::MultiDiscrete(v) => v.len(),
78 };
79
80 let init_obs = match envs.reset_batch(None) {
82 Ok(obs) => obs,
83 Err(_) => return,
84 };
85 let mut current_obs: Vec<f32> =
86 init_obs.into_iter().flat_map(|o| o.into_inner()).collect();
87
88 while !stop.load(Ordering::Relaxed) {
89 let total = n_steps * n_envs;
90
91 let mut all_obs = Vec::with_capacity(total * obs_dim);
92 let mut all_actions = Vec::with_capacity(total * act_dim);
93 let mut all_rewards = Vec::with_capacity(total);
94 let mut all_dones = Vec::with_capacity(total);
95 let mut all_values = Vec::with_capacity(total);
96 let mut all_log_probs = Vec::with_capacity(total);
97
98 let mut ok = true;
100 for _ in 0..n_steps {
101 if stop.load(Ordering::Relaxed) {
102 return;
103 }
104
105 let values = value_fn(¤t_obs);
107 let (actions_flat, log_probs) = action_fn(¤t_obs);
108
109 let actions: Vec<Action> = match envs.action_space() {
111 crate::env::spaces::ActionSpace::Discrete(_) => actions_flat
112 .iter()
113 .map(|&a| Action::Discrete(a as u32))
114 .collect(),
115 crate::env::spaces::ActionSpace::Box { shape, .. } => {
116 let dim: usize = shape.iter().product();
117 actions_flat
118 .chunks(dim)
119 .map(|chunk| Action::Continuous(chunk.to_vec()))
120 .collect()
121 }
122 crate::env::spaces::ActionSpace::MultiDiscrete(_) => actions_flat
123 .iter()
124 .map(|&a| Action::Discrete(a as u32))
125 .collect(),
126 };
127
128 all_obs.extend_from_slice(¤t_obs);
130 all_actions.extend_from_slice(&actions_flat);
131 all_values.extend(&values);
132 all_log_probs.extend(&log_probs);
133
134 match envs.step_batch(&actions) {
136 Ok(transition) => {
137 for i in 0..n_envs {
138 all_rewards.push(transition.rewards[i]);
139 let done = if transition.terminated[i] || transition.truncated[i] {
140 1.0
141 } else {
142 0.0
143 };
144 all_dones.push(done);
145 }
146 let mut offset = 0;
148 for obs_vec in transition.obs {
149 current_obs[offset..offset + obs_vec.len()]
150 .copy_from_slice(&obs_vec);
151 offset += obs_vec.len();
152 }
153 }
154 Err(_) => {
155 ok = false;
156 break;
157 }
158 }
159 }
160
161 if !ok {
162 break;
163 }
164
165 let last_values = value_fn(¤t_obs);
167
168 let mut env_major_rewards = vec![0.0; total];
170 let mut env_major_values = vec![0.0; total];
171 let mut env_major_dones = vec![0.0; total];
172 for t in 0..n_steps {
173 for e in 0..n_envs {
174 env_major_rewards[e * n_steps + t] = all_rewards[t * n_envs + e];
175 env_major_values[e * n_steps + t] = all_values[t * n_envs + e];
176 env_major_dones[e * n_steps + t] = all_dones[t * n_envs + e];
177 }
178 }
179
180 let (env_major_adv, env_major_ret) = gae::compute_gae_batched(
181 &env_major_rewards,
182 &env_major_values,
183 &env_major_dones,
184 &last_values,
185 n_steps,
186 gamma,
187 gae_lambda,
188 );
189
190 let mut advantages = vec![0.0; total];
192 let mut returns = vec![0.0; total];
193 for e in 0..n_envs {
194 for t in 0..n_steps {
195 advantages[t * n_envs + e] = env_major_adv[e * n_steps + t];
196 returns[t * n_envs + e] = env_major_ret[e * n_steps + t];
197 }
198 }
199
200 let batch = RolloutBatch {
201 observations: all_obs,
202 actions: all_actions,
203 rewards: all_rewards,
204 dones: all_dones,
205 log_probs: all_log_probs,
206 values: all_values,
207 advantages,
208 returns,
209 obs_dim,
210 act_dim,
211 n_steps,
212 n_envs,
213 };
214
215 let mut pending = Some(batch);
221 while let Some(b) = pending.take() {
222 if stop.load(Ordering::Relaxed) {
223 return;
224 }
225 match tx.send_timeout(b, SEND_POLL_INTERVAL) {
226 Ok(()) => {}
227 Err(SendTimeoutError::Timeout(b)) => pending = Some(b),
228 Err(SendTimeoutError::Disconnected(_)) => return, }
230 }
231 }
232 });
233
234 Self {
235 handle: Some(handle),
236 stop_flag,
237 }
238 }
239
240 pub fn stop(&mut self) {
242 self.stop_flag.store(true, Ordering::Relaxed);
243 if let Some(handle) = self.handle.take() {
244 let _ = handle.join();
245 }
246 }
247
248 pub fn is_stopped(&self) -> bool {
250 self.stop_flag.load(Ordering::Relaxed)
251 }
252}
253
254impl Drop for AsyncCollector {
255 fn drop(&mut self) {
256 self.stop();
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::env::builtins::CartPole;
264 use crate::env::parallel::VecEnv;
265 use crate::env::RLEnv;
266 use crate::pipeline::channel::Pipeline;
267 use crate::seed::derive_seed;
268
269 fn make_vec_env(n: usize, seed: u64) -> Box<dyn BatchSteppable> {
270 let envs: Vec<Box<dyn RLEnv>> = (0..n)
271 .map(|i| Box::new(CartPole::new(Some(derive_seed(seed, i)))) as Box<dyn RLEnv>)
272 .collect();
273 Box::new(VecEnv::new(envs).unwrap())
274 }
275
276 #[test]
277 fn test_async_collector_produces_batches() {
278 let pipe = Pipeline::new(4);
279 let tx = pipe.sender();
280
281 let value_fn: ValueFn = Arc::new(|obs: &[f32]| vec![0.0; obs.len() / 4]); let action_fn: ActionFn = Arc::new(|obs: &[f32]| {
283 let n = obs.len() / 4;
284 (vec![0.0; n], vec![0.0; n]) });
286
287 let mut collector = AsyncCollector::start(
288 make_vec_env(2, 42),
289 8, 0.99,
291 0.95,
292 tx,
293 value_fn,
294 action_fn,
295 );
296
297 let batch = pipe.recv().unwrap();
299 assert_eq!(batch.n_steps, 8);
300 assert_eq!(batch.n_envs, 2);
301 assert_eq!(batch.obs_dim, 4);
302 assert_eq!(batch.act_dim, 1);
303 assert_eq!(batch.observations.len(), 8 * 2 * 4);
304 assert_eq!(batch.rewards.len(), 8 * 2);
305 assert_eq!(batch.advantages.len(), 8 * 2);
306
307 collector.stop();
308 assert!(collector.is_stopped());
309 }
310
311 #[test]
312 fn test_async_collector_stop_is_idempotent() {
313 let pipe = Pipeline::new(2);
314 let tx = pipe.sender();
315
316 let value_fn: ValueFn = Arc::new(|obs: &[f32]| vec![0.0; obs.len() / 4]);
317 let action_fn: ActionFn = Arc::new(|obs: &[f32]| {
318 let n = obs.len() / 4;
319 (vec![0.0; n], vec![0.0; n])
320 });
321
322 let mut collector =
323 AsyncCollector::start(make_vec_env(1, 0), 4, 0.99, 0.95, tx, value_fn, action_fn);
324
325 collector.stop();
326 collector.stop(); }
328
329 #[test]
336 fn test_stop_terminates_when_channel_full_and_undrained() {
337 use crossbeam_channel::bounded;
338
339 let pipe = Pipeline::new(1);
342 let tx = pipe.sender();
343
344 let value_fn: ValueFn = Arc::new(|obs: &[f32]| vec![0.0; obs.len() / 4]);
345 let action_fn: ActionFn = Arc::new(|obs: &[f32]| {
346 let n = obs.len() / 4;
347 (vec![0.0; n], vec![0.0; n])
348 });
349
350 let mut collector =
351 AsyncCollector::start(make_vec_env(1, 7), 4, 0.99, 0.95, tx, value_fn, action_fn);
352
353 thread::sleep(Duration::from_millis(200));
355
356 let (done_tx, done_rx) = bounded::<()>(1);
359 let watchdog = thread::spawn(move || {
360 collector.stop();
361 let _ = done_tx.send(());
362 });
363
364 assert!(
365 done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
366 "AsyncCollector::stop() deadlocked on a full, undrained channel"
367 );
368 watchdog.join().unwrap();
369
370 drop(pipe);
373 }
374
375 #[test]
376 fn test_async_collector_gae_values_are_finite() {
377 let pipe = Pipeline::new(4);
378 let tx = pipe.sender();
379
380 let value_fn: ValueFn = Arc::new(|obs: &[f32]| vec![0.5; obs.len() / 4]);
381 let action_fn: ActionFn = Arc::new(|obs: &[f32]| {
382 let n = obs.len() / 4;
383 (vec![1.0; n], vec![-0.5; n])
384 });
385
386 let mut collector =
387 AsyncCollector::start(make_vec_env(4, 42), 16, 0.99, 0.95, tx, value_fn, action_fn);
388
389 let batch = pipe.recv().unwrap();
390 for &a in &batch.advantages {
391 assert!(a.is_finite(), "advantage must be finite, got {a}");
392 }
393 for &r in &batch.returns {
394 assert!(r.is_finite(), "return must be finite, got {r}");
395 }
396
397 collector.stop();
398 }
399}