Skip to content

Commit de5d3c1

Browse files
committed
Make unroll_length schedulable
1 parent de6a42f commit de5d3c1

3 files changed

Lines changed: 150 additions & 8 deletions

File tree

alf/algorithms/config.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import Optional, Callable
1616
import torch
1717
import alf
18-
from alf.utils.schedulers import as_scheduler
18+
from alf.utils.schedulers import ConstantScheduler, as_scheduler
1919

2020

2121
@alf.configurable
@@ -143,13 +143,18 @@ def __init__(self,
143143
total number of FRAMES will be (``num_env_steps*frame_skip``) for
144144
calculating sample efficiency. See alf/environments/wrappers.py
145145
for the definition of FrameSkip.
146-
unroll_length (float): number of time steps each environment proceeds per
147-
iteration. The total number of time steps from all environments per
148-
iteration can be computed as: ``num_envs * env_batch_size * unroll_length``.
149-
If ``unroll_length`` is not an integer, the actual unroll_length
146+
unroll_length (float|Scheduler): number of time steps each environment
147+
proceeds per iteration. The total number of time steps from all
148+
environments per iteration can be computed as:
149+
``num_envs * env_batch_size * unroll_length``. If
150+
``unroll_length`` is not an integer, the actual unroll_length
150151
being used will fluctuate between ``floor(unroll_length)`` and
151152
``ceil(unroll_length)`` and the expectation will be equal to
152-
``unroll_length``.
153+
``unroll_length``. For sync off-policy training,
154+
``unroll_length`` can also be a scheduler. In that case,
155+
``async_unroll`` and ``whole_replay_buffer_training`` must both
156+
be False. If a resolved value is 0, the iteration skips rollout
157+
and only performs replay-buffer updates.
153158
unroll_with_grad (bool): a bool flag indicating whether we require
154159
grad during ``unroll()``. This flag is only used by
155160
``OffPolicyAlgorithm`` where unrolling with grads is usually
@@ -389,6 +394,16 @@ def __init__(self,
389394
self.unroll_with_grad = unroll_with_grad
390395
self.use_root_inputs_for_after_train_iter = use_root_inputs_for_after_train_iter
391396
self.async_unroll = async_unroll
397+
if not isinstance(self._unroll_length, ConstantScheduler):
398+
assert not async_unroll, (
399+
"scheduled unroll_length is not supported for async_unroll=True"
400+
)
401+
assert not whole_replay_buffer_training, (
402+
"scheduled unroll_length is not supported for "
403+
"whole_replay_buffer_training=True")
404+
assert num_env_steps == 0, (
405+
"scheduled unroll_length is not supported when num_env_steps "
406+
"is used as a termination criterion")
392407
if async_unroll:
393408
assert not unroll_with_grad, ("unroll_with_grad is not supported "
394409
"for async_unroll=True")
@@ -455,3 +470,11 @@ def __init__(self,
455470
self.normalize_importance_weights_by_max = normalize_importance_weights_by_max
456471
self.visualize_alf_tree = visualize_alf_tree
457472
self.remote_training = remote_training
473+
474+
@property
475+
def unroll_length(self):
476+
return self._unroll_length()
477+
478+
@unroll_length.setter
479+
def unroll_length(self, value):
480+
self._unroll_length = as_scheduler(value)

alf/algorithms/rl_algorithm.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,11 @@ def _unroll_iter_off_policy(self):
813813
if not config.update_counter_every_mini_batch:
814814
alf.summary.increment_global_counter()
815815

816-
unroll_length = self._remaining_unroll_length_fraction + config.unroll_length
816+
# Preserve the configured value so we can distinguish it from the
817+
# integerized length after carrying over any fractional remainder.
818+
requested_unroll_length = config.unroll_length
819+
unroll_length = (self._remaining_unroll_length_fraction +
820+
requested_unroll_length)
817821
self._remaining_unroll_length_fraction = unroll_length - int(
818822
unroll_length)
819823
unroll_length = int(unroll_length)
@@ -823,9 +827,13 @@ def _unroll_iter_off_policy(self):
823827
unrolled = False
824828
root_inputs = None
825829
rollout_info = None
830+
# Async unroll still needs one unroll call to pump queued work even when
831+
# the configured unroll length is exactly zero.
832+
allow_zero_length_unroll = (config.async_unroll
833+
and requested_unroll_length == 0)
826834
if (alf.summary.get_global_counter()
827835
>= self._rl_train_after_update_steps
828-
and (unroll_length > 0 or config.unroll_length == 0) and
836+
and (unroll_length > 0 or allow_zero_length_unroll) and
829837
(config.num_env_steps == 0
830838
or self.get_step_metrics()[1].result() < config.num_env_steps)):
831839
unrolled = True

alf/algorithms/rl_algorithm_test.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import alf
2121
from alf.utils import common, dist_utils, tensor_utils
22+
from alf.utils.schedulers import StepScheduler, update_progress
2223
from alf.data_structures import AlgStep, Experience, LossInfo, StepType, TimeStep
2324
from alf.algorithms.rl_algorithm import RLAlgorithm
2425
from alf.algorithms.config import TrainerConfig
@@ -174,6 +175,45 @@ def current_time_step(self):
174175

175176
class 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

Comments
 (0)