Files
2026-07-13 13:17:40 +08:00

155 lines
5.8 KiB
Python

import unittest
import numpy as np
from gymnasium.spaces import Box, Discrete
from ray.rllib.algorithms.impala.tests.test_vtrace_old_api_stack import (
_ground_truth_vtrace_calculation,
)
from ray.rllib.algorithms.impala.torch.vtrace_torch_v2 import (
make_time_major,
vtrace_torch,
)
from ray.rllib.core.distribution.torch.torch_distribution import TorchCategorical
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.test_utils import check
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
torch, _ = try_import_torch()
def flatten_batch_and_time_dim(t):
if not torch.is_tensor(t):
t = torch.from_numpy(t)
new_shape = [-1] + list(t.shape[2:])
return torch.reshape(t, new_shape)
class TestVtraceRLModule(unittest.TestCase):
"""Tests V-trace-v2 against ground truth data calculated.
There is a ground truth implementation that we used to test our original
implementation against. This test checks that the new implementation still
matches the ground truth test from our first implementation of V-Trace.
"""
@classmethod
def setUpClass(cls):
"""Sets up inputs for V-Trace and calculate ground truth.
We use tf operations here to compile the inputs but convert to numpy arrays to
calculate the ground truth (and the other v-trace outputs in the
framework-specific tests).
"""
# we can test against any trajectory length or batch size and it won't matter
trajectory_len = 5
batch_size = 10
action_space = Discrete(10)
action_logit_space = Box(-1.0, 1.0, (action_space.n,), np.float32)
behavior_action_logits = torch.from_numpy(
np.array([action_logit_space.sample()], dtype=np.float32)
)
target_action_logits = torch.from_numpy(
np.array([action_logit_space.sample()], dtype=np.float32)
)
behavior_dist = TorchCategorical(logits=behavior_action_logits)
target_dist = TorchCategorical(logits=target_action_logits)
dummy_action_batch = [
[action_space.sample() for _ in range(trajectory_len)]
for _ in range(batch_size)
]
behavior_log_probs = torch.stack(
[
torch.squeeze(
behavior_dist.logp(torch.from_numpy(np.array(v, np.uint8)))
)
for v in dummy_action_batch
]
)
target_log_probs = torch.stack(
[
torch.squeeze(target_dist.logp(torch.from_numpy(np.array(v, np.uint8))))
for v in dummy_action_batch
]
)
# target_log_probs = torch.stack(
# tree.map_structure(
# lambda v: torch.squeeze(target_dist.logp(v)), dummy_action_batch
# )
# )
value_fn_space_w_time = Box(-1.0, 1.0, (batch_size, trajectory_len), np.float32)
value_fn_space = Box(-1.0, 1.0, (batch_size,), np.float32)
# using randomly sampled values in lieu of actual values sampled from a value fn
values = value_fn_space_w_time.sample()
# this is supposed to be the value function at the last timestep of each
# trajectory in the batch. In IMPALA its bootstrapped at training time
cls.bootstrap_values = np.array(value_fn_space.sample() + 1.0)
# discount factor used at all of the timesteps
discounts = torch.from_numpy(
np.array([0.9 for _ in range(trajectory_len * batch_size)])
)
rewards = value_fn_space_w_time.sample()
cls.clip_rho_threshold = 3.7
cls.clip_pg_rho_threshold = 2.2
# convert to time major dimension
cls.behavior_log_probs_time_major = make_time_major(
flatten_batch_and_time_dim(behavior_log_probs),
trajectory_len=trajectory_len,
).numpy()
cls.target_log_probs_time_major = make_time_major(
flatten_batch_and_time_dim(target_log_probs), trajectory_len=trajectory_len
).numpy()
cls.discounts_time_major = make_time_major(
flatten_batch_and_time_dim(discounts), trajectory_len=trajectory_len
).numpy()
cls.rewards_time_major = make_time_major(
flatten_batch_and_time_dim(rewards), trajectory_len=trajectory_len
).numpy()
cls.values_time_major = make_time_major(
flatten_batch_and_time_dim(values), trajectory_len=trajectory_len
).numpy()
log_rhos = cls.target_log_probs_time_major - cls.behavior_log_probs_time_major
cls.ground_truth_v = _ground_truth_vtrace_calculation(
discounts=cls.discounts_time_major,
log_rhos=log_rhos,
rewards=cls.rewards_time_major,
values=cls.values_time_major,
bootstrap_value=cls.bootstrap_values,
clip_rho_threshold=cls.clip_rho_threshold,
clip_pg_rho_threshold=cls.clip_pg_rho_threshold,
)
def test_vtrace_torch(self):
output_torch_vtrace = vtrace_torch(
behaviour_action_log_probs=convert_to_torch_tensor(
self.behavior_log_probs_time_major
),
target_action_log_probs=convert_to_torch_tensor(
self.target_log_probs_time_major
),
discounts=convert_to_torch_tensor(self.discounts_time_major),
rewards=convert_to_torch_tensor(self.rewards_time_major),
values=convert_to_torch_tensor(self.values_time_major),
bootstrap_values=convert_to_torch_tensor(self.bootstrap_values),
clip_rho_threshold=self.clip_rho_threshold,
clip_pg_rho_threshold=self.clip_pg_rho_threshold,
)
check(output_torch_vtrace, self.ground_truth_v)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))