1919
2020import alf
2121from alf .utils import common , dist_utils , tensor_utils
22+ from alf .utils .schedulers import StepScheduler , update_progress
2223from alf .data_structures import AlgStep , Experience , LossInfo , StepType , TimeStep
2324from alf .algorithms .rl_algorithm import RLAlgorithm
2425from alf .algorithms .config import TrainerConfig
@@ -174,6 +175,45 @@ def current_time_step(self):
174175
175176class RLAlgorithmTest (unittest .TestCase ):
176177
178+ class _ReplayOnlyAlg (MyAlg ):
179+
180+ def __init__ (self , config ):
181+ observation_spec = TensorSpec ((2 , ), dtype = 'float32' )
182+ action_spec = alf .BoundedTensorSpec (shape = (),
183+ dtype = 'int64' ,
184+ minimum = 0 ,
185+ maximum = 2 )
186+ super ().__init__ (observation_spec = observation_spec ,
187+ action_spec = action_spec ,
188+ env = None ,
189+ config = config ,
190+ on_policy = False )
191+ # A non-None sentinel is enough to make RLAlgorithm treat this as
192+ # replay-buffer-backed during off-policy training.
193+ self ._replay_buffer = object ()
194+ # These counters let the test assert whether rollout work was
195+ # skipped and whether replay-only hooks still ran.
196+ self .unroll_calls = []
197+ self .train_calls = 0
198+ self .after_train_iter_calls = 0
199+
200+ def _unroll (self , unroll_length : int ):
201+ self .unroll_calls .append (unroll_length )
202+ return None
203+
204+ def train_from_replay_buffer (self , update_global_counter = False ):
205+ # Return a fixed step count so the test can focus on control flow
206+ # rather than replay buffer contents.
207+ self .train_calls += 1
208+ self .update_global_counter = update_global_counter
209+ return 7
210+
211+ def after_train_iter (self , root_inputs , train_info ):
212+ self .after_train_iter_calls += 1
213+
214+ def tearDown (self ):
215+ update_progress ('iterations' , 0 )
216+
177217 def test_on_policy_algorithm (self ):
178218 # root_dir is not used. We have to give it a value because
179219 # it is a required argument of TrainerConfig.
@@ -198,6 +238,77 @@ def test_on_policy_algorithm(self):
198238 self .assertTrue (torch .all (logits [1 , :] > logits [0 , :]))
199239 self .assertTrue (torch .all (logits [1 , :] > logits [2 , :]))
200240
241+ def test_scheduled_unroll_length_guards (self ):
242+ unroll_length = StepScheduler ('iterations' , [(1 , 1 ), (2 , 0 )])
243+
244+ with self .assertRaisesRegex (
245+ AssertionError ,
246+ "scheduled unroll_length is not supported for async_unroll=True"
247+ ):
248+ TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
249+ unroll_length = unroll_length ,
250+ async_unroll = True ,
251+ max_unroll_length = 1 )
252+
253+ with self .assertRaisesRegex (
254+ AssertionError , "scheduled unroll_length is not supported for "
255+ "whole_replay_buffer_training=True" ):
256+ TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
257+ unroll_length = unroll_length ,
258+ whole_replay_buffer_training = True )
259+
260+ with self .assertRaisesRegex (
261+ AssertionError ,
262+ "scheduled unroll_length is not supported when num_env_steps "
263+ "is used as a termination criterion" ):
264+ TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
265+ unroll_length = unroll_length ,
266+ num_env_steps = 1 ,
267+ num_iterations = 0 ,
268+ whole_replay_buffer_training = False )
269+
270+ def test_scheduled_zero_unroll_skips_rollout (self ):
271+ config = TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
272+ unroll_length = StepScheduler (
273+ 'iterations' , [(1 , 1 ), (2 , 0 )]),
274+ mini_batch_length = 1 ,
275+ mini_batch_size = 1 ,
276+ whole_replay_buffer_training = False )
277+ alg = self ._ReplayOnlyAlg (config )
278+
279+ update_progress ('iterations' , 0 )
280+ self .assertEqual (alg ._train_iter_off_policy (), 7 )
281+ self .assertEqual (alg .unroll_calls , [1 ])
282+ self .assertEqual (alg .train_calls , 1 )
283+ self .assertEqual (alg .after_train_iter_calls , 1 )
284+ self .assertTrue (alg .update_global_counter )
285+
286+ update_progress ('iterations' , 1 )
287+ self .assertEqual (alg ._train_iter_off_policy (), 7 )
288+ self .assertEqual (alg .unroll_calls , [1 ])
289+ self .assertEqual (alg .train_calls , 2 )
290+ self .assertEqual (alg .after_train_iter_calls , 1 )
291+
292+ def test_constant_unroll_length_keeps_scalar_behavior (self ):
293+ config = TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
294+ unroll_length = 5 ,
295+ async_unroll = True ,
296+ max_unroll_length = 5 )
297+ self .assertEqual (config .unroll_length , 5 )
298+ self .assertEqual (config .max_unroll_length , 5 )
299+
300+ def test_on_policy_constant_unroll_length_still_works (self ):
301+ config = TrainerConfig (root_dir = '/tmp/rl_algorithm_test' ,
302+ unroll_length = 3 )
303+ env = MyEnv (batch_size = 2 )
304+ alg = MyAlg (observation_spec = env .observation_spec (),
305+ action_spec = env .action_spec (),
306+ env = env ,
307+ config = config ,
308+ on_policy = True )
309+ steps = alg .train_iter ()
310+ self .assertEqual (steps , 6 )
311+
201312 def test_off_policy_algorithm (self ):
202313 with tempfile .TemporaryDirectory () as root_dir :
203314 common .run_under_record_context (
0 commit comments