11from collections import defaultdict
22from typing import Any , Dict , List , Optional
3+ from concurrent .futures import ThreadPoolExecutor , as_completed
34
45import torch
56import wandb
1011class ActorCriticTrainerBase :
1112 """Shared training utilities for actor-critic style trainers."""
1213
14+ def _parallel_agent_mode_enabled (self ) -> bool :
15+ if str (getattr (self , "parallel_training" , "" )).lower () != "mp" :
16+ return False
17+ num_agents = int (getattr (getattr (self , "args" , None ), "num_agents" , 0 ) or 0 )
18+ if num_agents <= 1 :
19+ return False
20+ devices = getattr (self , "agent_devices" , None )
21+ if not devices :
22+ return True
23+ unique = {str (device ) for device in devices }
24+ return len (unique ) > 1
25+
26+ def _run_agent_tasks (
27+ self ,
28+ fn ,
29+ * ,
30+ agent_indices : Optional [List [int ]] = None ,
31+ parallel : Optional [bool ] = None ,
32+ ) -> List [Any ]:
33+ num_agents = int (getattr (getattr (self , "args" , None ), "num_agents" , 0 ) or 0 )
34+ indices = (
35+ list (agent_indices )
36+ if agent_indices is not None
37+ else list (range (max (num_agents , 0 )))
38+ )
39+ if not indices :
40+ return []
41+
42+ use_parallel = (
43+ self ._parallel_agent_mode_enabled () if parallel is None else bool (parallel )
44+ )
45+ if not use_parallel or len (indices ) <= 1 :
46+ return [fn (agent_idx ) for agent_idx in indices ]
47+
48+ results : Dict [int , Any ] = {}
49+ max_workers = len (indices )
50+ with ThreadPoolExecutor (max_workers = max_workers ) as executor :
51+ futures = {
52+ executor .submit (fn , agent_idx ): agent_idx for agent_idx in indices
53+ }
54+ for future in as_completed (futures ):
55+ agent_idx = futures [future ]
56+ results [agent_idx ] = future .result ()
57+ return [results [agent_idx ] for agent_idx in indices ]
58+
1359 def _filter_model_kwargs (self , cfg : Optional [Dict [str , Any ]]) -> Dict [str , Any ]:
1460 torch_dtype = None
1561 if isinstance (cfg , dict ):
@@ -64,16 +110,18 @@ def _encode_prompt(
64110 prompt : str ,
65111 agent_idx : Optional [int ] = None ,
66112 tokenizer : Optional [Any ] = None ,
113+ device : Optional [torch .device ] = None ,
67114 ) -> Dict [str , torch .Tensor ]:
68115 tokenizer = tokenizer or self ._get_tokenizer (agent_idx )
69116 encoded = tokenizer (
70117 prompt ,
71118 return_tensors = "pt" ,
72119 truncation = True ,
73120 )
121+ target_device = device or self .device
74122 return {
75- "input_ids" : encoded ["input_ids" ].to (self . device ),
76- "attention_mask" : encoded ["attention_mask" ].to (self . device ),
123+ "input_ids" : encoded ["input_ids" ].to (target_device ),
124+ "attention_mask" : encoded ["attention_mask" ].to (target_device ),
77125 }
78126
79127 def _prepare_advantages (self , rollouts : List [Any ]) -> None :
@@ -139,7 +187,9 @@ def _summarize_rollout_metrics(self, rollouts: List[Any]) -> Dict[str, float]:
139187 return metrics
140188
141189 def _iter_dataloader (self , dataloader , epoch : int , total_epochs : int ):
142- if getattr (self , "verbose" , True ):
190+ dist_env = getattr (self , "dist_env" , None )
191+ is_main = bool (getattr (dist_env , "is_main" , True ))
192+ if getattr (self , "verbose" , True ) and is_main :
143193 return enumerate (
144194 tqdm (
145195 dataloader ,
@@ -165,7 +215,12 @@ def _on_epoch_end(
165215 epoch_metrics : Dict [str , List [float ]],
166216 ) -> None :
167217 summary = self ._summarize_epoch_metrics (epoch_metrics )
168- if summary and getattr (self , "verbose" , True ):
218+ dist_env = getattr (self , "dist_env" , None )
219+ if (
220+ summary
221+ and getattr (self , "verbose" , True )
222+ and getattr (dist_env , "is_main" , True )
223+ ):
169224 print (f"Epoch { epoch + 1 } /{ total_epochs } metrics: { summary } " )
170225
171226 def _tag_metrics (
@@ -197,10 +252,9 @@ def _process_buffer(
197252 self ,
198253 agent_idx : int ,
199254 buffer : List [Any ],
200- epoch_metrics : Dict [str , List [float ]],
201- ) -> None :
255+ ) -> Dict [str , Any ]:
202256 if not buffer :
203- return
257+ return { "metric_values" : {}, "log_metrics" : {}}
204258
205259 has_turn_idx = any (
206260 "turn_idx" in (getattr (s , "metadata" , {}) or {}) for s in buffer
@@ -213,35 +267,74 @@ def _process_buffer(
213267 buffer .clear ()
214268
215269 combined_log : Dict [str , float ] = {}
270+ metric_values : Dict [str , List [float ]] = {}
216271 for t_idx in sorted (turn_groups .keys ()):
217272 samples = turn_groups [t_idx ]
218273 metrics = self ._update (agent_idx , samples )
219274 tagged = self ._tag_metrics (metrics , agent_idx , turn_idx = t_idx )
220275 combined_log .update (tagged )
221276 for key , value in tagged .items ():
222- epoch_metrics [key ].append (value )
277+ metric_values .setdefault (key , []).append (value )
278+ return {"metric_values" : metric_values , "log_metrics" : combined_log }
279+
280+ def _drain_ready_agent_buffers (
281+ self ,
282+ ready_agents : List [int ],
283+ epoch_metrics : Dict [str , List [float ]],
284+ ) -> None :
285+ if not ready_agents :
286+ return
287+
288+ unique_ready = sorted ({int (idx ) for idx in ready_agents })
289+ run_parallel = bool (
290+ getattr (
291+ self ,
292+ "_parallel_update_enabled" ,
293+ self ._parallel_agent_mode_enabled (),
294+ )
295+ )
296+
297+ def _process (agent_idx : int ) -> Dict [str , Any ]:
298+ return self ._process_buffer (agent_idx , self .rollout_buffers [agent_idx ])
299+
300+ results = self ._run_agent_tasks (
301+ _process ,
302+ agent_indices = unique_ready ,
303+ parallel = run_parallel ,
304+ )
305+
306+ combined_log : Dict [str , float ] = {}
307+ for result in results :
308+ metric_values = result .get ("metric_values" , {})
309+ for key , values in metric_values .items ():
310+ for value in values :
311+ epoch_metrics [key ].append (value )
312+ combined_log .update (result .get ("log_metrics" , {}))
223313
224314 if combined_log and self ._should_log_train ():
225315 self ._log_metrics (combined_log )
226316
227317 def _run_batch (self , batch , epoch_metrics : Dict [str , List [float ]]) -> None :
228318 for item in batch :
229319 rollouts = self ._collect_rollouts (item )
320+ ready_agents : List [int ] = []
230321 for sample in rollouts :
231322 agent_idx = sample .agent_idx
232323 buffer = self .rollout_buffers [agent_idx ]
233324 buffer .append (sample )
234325 if len (buffer ) >= self .args .rollout_buffer_size :
235- self ._process_buffer (agent_idx , buffer , epoch_metrics )
326+ ready_agents .append (agent_idx )
327+ if ready_agents :
328+ self ._drain_ready_agent_buffers (ready_agents , epoch_metrics )
236329 if self .args .num_agents > 0 :
237330 # Count joint-action reward evaluations (one per agent group).
238331 self .env_step += len (rollouts ) // self .args .num_agents
239332
240333 def _flush_buffers (self , epoch_metrics : Dict [str , List [float ]]) -> None :
241- for agent_idx , buffer in enumerate ( self . rollout_buffers ):
242- if not buffer :
243- continue
244- self ._process_buffer ( agent_idx , buffer , epoch_metrics )
334+ ready_agents = [
335+ agent_idx for agent_idx , buffer in enumerate ( self . rollout_buffers ) if buffer
336+ ]
337+ self ._drain_ready_agent_buffers ( ready_agents , epoch_metrics )
245338
246339 def get_train_dataloader (self ) -> DataLoader :
247340 if self .train_dataset is None :
@@ -275,18 +368,22 @@ def evaluate(self) -> Dict[str, float]:
275368 turn_groups : Dict [int , List [Any ]] = {}
276369 seen = 0
277370
278- with torch .no_grad ():
279- for batch in dataloader :
280- for item in batch :
281- rollouts = self ._collect_rollouts (item )
282- for sample in rollouts :
283- t_idx = int (sample .metadata .get ("turn_idx" , 0 ))
284- turn_groups .setdefault (t_idx , []).append (sample )
285- seen += 1
371+ self ._in_eval = True
372+ try :
373+ with torch .no_grad ():
374+ for batch in dataloader :
375+ for item in batch :
376+ rollouts = self ._collect_rollouts (item )
377+ for sample in rollouts :
378+ t_idx = int (sample .metadata .get ("turn_idx" , 0 ))
379+ turn_groups .setdefault (t_idx , []).append (sample )
380+ seen += 1
381+ if seen >= num_samples :
382+ break
286383 if seen >= num_samples :
287384 break
288- if seen >= num_samples :
289- break
385+ finally :
386+ self . _in_eval = False
290387
291388 eval_log : Dict [str , float ] = {}
292389 for turn_idx , samples in sorted (turn_groups .items ()):
0 commit comments