|
17 | 17 | from torchrl.data import ReplayBuffer, TensorDictReplayBuffer |
18 | 18 | from torchrl.data.replay_buffers.samplers import RandomSampler, SliceSampler |
19 | 19 | from torchrl.data.replay_buffers.storages import LazyMemmapStorage, LazyTensorStorage |
20 | | -from torchrl.envs.transforms import NextStateReconstructor |
| 20 | +from torchrl.envs.transforms import NextStateReconstructor, PolicyAgeFilter |
21 | 21 | from torchrl.envs.transforms.transforms import ( |
22 | 22 | BinarizeReward, |
23 | 23 | CatFrames, |
@@ -511,6 +511,99 @@ def test_bad_batch_dims_errors(self): |
511 | 511 | NextStateReconstructor()(td) |
512 | 512 |
|
513 | 513 |
|
| 514 | +class TestPolicyAgeFilter: |
| 515 | + @staticmethod |
| 516 | + def _data(versions): |
| 517 | + versions = torch.as_tensor(versions) |
| 518 | + return TensorDict( |
| 519 | + { |
| 520 | + "observation": torch.randn(versions.shape[0], 3), |
| 521 | + "policy_version": versions, |
| 522 | + }, |
| 523 | + batch_size=[versions.shape[0]], |
| 524 | + ) |
| 525 | + |
| 526 | + def test_filters_on_extend(self): |
| 527 | + rb = ReplayBuffer( |
| 528 | + storage=LazyTensorStorage(100), |
| 529 | + transform=PolicyAgeFilter(3, max_policy_lag=1), |
| 530 | + ) |
| 531 | + rb.extend(self._data([0, 2, 2, 3])) |
| 532 | + assert len(rb) == 3 |
| 533 | + |
| 534 | + def test_filters_dynamically_on_sample(self): |
| 535 | + current = {"version": 3} |
| 536 | + rb = ReplayBuffer( |
| 537 | + storage=LazyTensorStorage(100), |
| 538 | + transform=PolicyAgeFilter(lambda: current["version"], max_policy_lag=1), |
| 539 | + ) |
| 540 | + rb.extend(self._data([2, 2, 3, 3])) |
| 541 | + assert len(rb) == 4 |
| 542 | + assert rb.sample(4).batch_size[0] == 4 |
| 543 | + # The policy moved on: version-2 data became stale since insertion. |
| 544 | + # The random sampler draws with replacement, so the batch size is |
| 545 | + # not deterministic; the invariant is that no stale element passes. |
| 546 | + current["version"] = 4 |
| 547 | + sample = rb.sample(4) |
| 548 | + assert (sample["policy_version"] == 3).all() |
| 549 | + # Once everything is stale the batch is deterministically empty. |
| 550 | + current["version"] = 10 |
| 551 | + sample = rb.sample(4) |
| 552 | + assert sample.batch_size[0] == 0 |
| 553 | + |
| 554 | + def test_nested_version_key(self): |
| 555 | + rb = ReplayBuffer( |
| 556 | + storage=LazyTensorStorage(100), |
| 557 | + transform=PolicyAgeFilter( |
| 558 | + 3, max_policy_lag=0, policy_version_key=("meta", "policy_version") |
| 559 | + ), |
| 560 | + ) |
| 561 | + data = TensorDict( |
| 562 | + { |
| 563 | + "observation": torch.randn(3, 2), |
| 564 | + ("meta", "policy_version"): torch.tensor([1, 3, 3]), |
| 565 | + }, |
| 566 | + batch_size=[3], |
| 567 | + ) |
| 568 | + rb.extend(data) |
| 569 | + assert len(rb) == 2 |
| 570 | + |
| 571 | + def test_row_uses_oldest_element(self): |
| 572 | + # [B, T] data: a single stale element marks the whole row stale |
| 573 | + filt = PolicyAgeFilter(5, max_policy_lag=2) |
| 574 | + td = TensorDict( |
| 575 | + { |
| 576 | + "observation": torch.randn(2, 4), |
| 577 | + "policy_version": torch.tensor([[3, 5, 5, 5], [4, 5, 5, 2]]), |
| 578 | + }, |
| 579 | + batch_size=[2, 4], |
| 580 | + ) |
| 581 | + out = filt(td) |
| 582 | + assert out.batch_size[0] == 1 |
| 583 | + |
| 584 | + def test_missing_key_warns_and_passes_through(self): |
| 585 | + filt = PolicyAgeFilter(3, max_policy_lag=1) |
| 586 | + td = TensorDict({"observation": torch.randn(4, 3)}, batch_size=[4]) |
| 587 | + out = filt(td) |
| 588 | + assert out.batch_size[0] == 4 |
| 589 | + with pytest.raises(KeyError, match="PolicyAgeFilter"): |
| 590 | + PolicyAgeFilter(3, max_policy_lag=1, strict=True)(td) |
| 591 | + |
| 592 | + def test_negative_lag_rejected(self): |
| 593 | + with pytest.raises(ValueError, match="non-negative"): |
| 594 | + PolicyAgeFilter(3, max_policy_lag=-1) |
| 595 | + |
| 596 | + def test_noop_when_attached_to_env(self): |
| 597 | + from torchrl.envs import TransformedEnv |
| 598 | + from torchrl.testing.mocking_classes import CountingEnv |
| 599 | + |
| 600 | + env = TransformedEnv( |
| 601 | + CountingEnv(max_steps=4), PolicyAgeFilter(0, max_policy_lag=0) |
| 602 | + ) |
| 603 | + rollout = env.rollout(3) |
| 604 | + assert rollout.batch_size[0] == 3 |
| 605 | + |
| 606 | + |
514 | 607 | if __name__ == "__main__": |
515 | 608 | args, unknown = argparse.ArgumentParser().parse_known_args() |
516 | 609 | pytest.main([__file__, "--capture", "no", "--exitfirst"] + unknown) |
0 commit comments