[RLlib] Add a minimal JAX ModelV2 (FCNet) to RLlib. (#12502)

This commit is contained in:
Sven Mika
2020-12-03 15:51:30 +01:00
committed by GitHub
parent ff34563539
commit 3f4bc16276
13 changed files with 444 additions and 58 deletions
+44 -12
View File
@@ -8,6 +8,7 @@ from typing import List, Optional, Type, Union
from ray.tune.registry import RLLIB_MODEL, RLLIB_PREPROCESSOR, \
RLLIB_ACTION_DIST, _global_registry
from ray.rllib.models.action_dist import ActionDistribution
from ray.rllib.models.jax.jax_action_dist import JAXCategorical
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.preprocessors import get_preprocessor, Preprocessor
from ray.rllib.models.tf.recurrent_net import LSTMWrapper
@@ -131,7 +132,7 @@ class ModelCatalog:
dist_type (Optional[Union[str, Type[ActionDistribution]]]):
Identifier of the action distribution (str) interpreted as a
hint or the actual ActionDistribution class to use.
framework (str): One of "tf", "tfe", or "torch".
framework (str): One of "tf2", "tf", "tfe", "torch", or "jax".
kwargs (dict): Optional kwargs to pass on to the Distribution's
constructor.
@@ -179,8 +180,8 @@ class ModelCatalog:
else Deterministic
# Discrete Space -> Categorical.
elif isinstance(action_space, gym.spaces.Discrete):
dist_cls = (TorchCategorical
if framework == "torch" else Categorical)
dist_cls = TorchCategorical if framework == "torch" else \
JAXCategorical if framework == "jax" else Categorical
# Tuple/Dict Spaces -> MultiAction.
elif dist_type in (MultiActionDistribution,
TorchMultiActionDistribution) or \
@@ -282,7 +283,7 @@ class ModelCatalog:
unflatten the tensor into a ragged tensor.
action_space (Space): Action space of the target gym env.
num_outputs (int): The size of the output vector of the model.
framework (str): One of "tf", "tfe", or "torch".
framework (str): One of "tf2", "tf", "tfe", "torch", or "jax".
name (str): Name (scope) for the model.
model_interface (cls): Interface required for the model
default_model (cls): Override the default class for the model. This
@@ -294,7 +295,6 @@ class ModelCatalog:
"""
if model_config.get("custom_model"):
# Allow model kwargs to be overridden / augmented by
# custom_model_config.
customized_model_kwargs = dict(
@@ -358,7 +358,7 @@ class ModelCatalog:
"model.register_variables() on the variables in "
"question?".format(not_registered, instance,
registered))
else:
elif framework == "torch":
# PyTorch automatically tracks nn.Modules inside the parent
# nn.Module's constructor.
# Try calling with kwargs first (custom ModelV2 should
@@ -381,8 +381,14 @@ class ModelCatalog:
# Other error -> re-raise.
else:
raise e
else:
raise NotImplementedError(
"`framework` must be 'tf2|tf|tfe|torch', but is "
"{}!".format(framework))
return instance
# Find a default TFModelV2 and wrap with model_interface.
if framework in ["tf", "tfe", "tf2"]:
v2_class = None
# Try to get a default v2 model.
@@ -404,6 +410,8 @@ class ModelCatalog:
wrapper = ModelCatalog._wrap_if_needed(v2_class, model_interface)
return wrapper(obs_space, action_space, num_outputs, model_config,
name, **model_kwargs)
# Find a default TorchModelV2 and wrap with model_interface.
elif framework == "torch":
v2_class = \
default_model or ModelCatalog._get_v2_model_class(
@@ -420,6 +428,18 @@ class ModelCatalog:
wrapper = ModelCatalog._wrap_if_needed(v2_class, model_interface)
return wrapper(obs_space, action_space, num_outputs, model_config,
name, **model_kwargs)
# Find a default JAXModelV2 and wrap with model_interface.
elif framework == "jax":
v2_class = \
default_model or ModelCatalog._get_v2_model_class(
obs_space, model_config, framework=framework)
if model_config.get("use_lstm"):
raise NotImplementedError("JAXModel's not LSTM-wrappable yet!")
# Wrap in the requested interface.
wrapper = ModelCatalog._wrap_if_needed(v2_class, model_interface)
return wrapper(obs_space, action_space, num_outputs, model_config,
name, **model_kwargs)
else:
raise NotImplementedError(
"`framework` must be 'tf2|tf|tfe|torch', but is "
@@ -542,16 +562,26 @@ class ModelCatalog:
def _get_v2_model_class(input_space: gym.Space,
model_config: ModelConfigDict,
framework: str = "tf") -> ModelV2:
if framework == "torch":
from ray.rllib.models.torch.fcnet import (FullyConnectedNetwork as
FCNet)
from ray.rllib.models.torch.visionnet import (VisionNetwork as
VisionNet)
else:
VisionNet = None
if framework in ["tf2", "tf", "tfe"]:
from ray.rllib.models.tf.fcnet import \
FullyConnectedNetwork as FCNet
from ray.rllib.models.tf.visionnet import \
VisionNetwork as VisionNet
elif framework == "torch":
from ray.rllib.models.torch.fcnet import (FullyConnectedNetwork as
FCNet)
from ray.rllib.models.torch.visionnet import (VisionNetwork as
VisionNet)
elif framework == "jax":
from ray.rllib.models.jax.fcnet import (FullyConnectedNetwork as
FCNet)
else:
raise ValueError(
"framework={} not supported in `ModelCatalog._get_v2_model_"
"class`!".format(framework))
# Discrete/1D obs-spaces.
if isinstance(input_space, gym.spaces.Discrete) or \
@@ -559,6 +589,8 @@ class ModelCatalog:
return FCNet
# Default Conv2D net.
else:
if framework == "jax":
raise NotImplementedError("No Conv2D default net for JAX yet!")
return VisionNet
@staticmethod
View File
+125
View File
@@ -0,0 +1,125 @@
import logging
import numpy as np
import time
from ray.rllib.models.jax.jax_modelv2 import JAXModelV2
from ray.rllib.models.jax.misc import SlimFC
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_jax
jax, flax = try_import_jax()
logger = logging.getLogger(__name__)
class FullyConnectedNetwork(JAXModelV2):
"""Generic fully connected network."""
def __init__(self, obs_space, action_space, num_outputs, model_config,
name):
super().__init__(obs_space, action_space, num_outputs, model_config,
name)
self.key = jax.random.PRNGKey(int(time.time()))
activation = model_config.get("fcnet_activation")
hiddens = model_config.get("fcnet_hiddens", [])
no_final_linear = model_config.get("no_final_linear")
self.vf_share_layers = model_config.get("vf_share_layers")
self.free_log_std = model_config.get("free_log_std")
# Generate free-floating bias variables for the second half of
# the outputs.
if self.free_log_std:
assert num_outputs % 2 == 0, (
"num_outputs must be divisible by two", num_outputs)
num_outputs = num_outputs // 2
self._hidden_layers = []
prev_layer_size = int(np.product(obs_space.shape))
self._logits = None
# Create layers 0 to second-last.
for size in hiddens[:-1]:
self._hidden_layers.append(
SlimFC(
in_size=prev_layer_size,
out_size=size,
activation_fn=activation))
prev_layer_size = size
# The last layer is adjusted to be of size num_outputs, but it's a
# layer with activation.
if no_final_linear and num_outputs:
self._hidden_layers.append(
SlimFC(
in_size=prev_layer_size,
out_size=num_outputs,
activation_fn=activation))
prev_layer_size = num_outputs
# Finish the layers with the provided sizes (`hiddens`), plus -
# iff num_outputs > 0 - a last linear layer of size num_outputs.
else:
if len(hiddens) > 0:
self._hidden_layers.append(
SlimFC(
in_size=prev_layer_size,
out_size=hiddens[-1],
activation_fn=activation))
prev_layer_size = hiddens[-1]
if num_outputs:
self._logits = SlimFC(
in_size=prev_layer_size,
out_size=num_outputs,
activation_fn=None)
else:
self.num_outputs = (
[int(np.product(obs_space.shape))] + hiddens[-1:])[-1]
# Layer to add the log std vars to the state-dependent means.
if self.free_log_std and self._logits:
raise ValueError("`free_log_std` not supported for JAX yet!")
self._value_branch_separate = None
if not self.vf_share_layers:
# Build a parallel set of hidden layers for the value net.
prev_vf_layer_size = int(np.product(obs_space.shape))
vf_layers = []
for size in hiddens:
vf_layers.append(
SlimFC(
in_size=prev_vf_layer_size,
out_size=size,
activation_fn=activation,
))
prev_vf_layer_size = size
self._value_branch_separate = vf_layers
self._value_branch = SlimFC(
in_size=prev_layer_size, out_size=1, activation_fn=None)
# Holds the current "base" output (before logits layer).
self._features = None
# Holds the last input, in case value branch is separate.
self._last_flat_in = None
@override(JAXModelV2)
def forward(self, input_dict, state, seq_lens):
self._last_flat_in = input_dict["obs_flat"]
x = self._last_flat_in
for layer in self._hidden_layers:
x = layer(x)
self._features = x
logits = self._logits(self._features) if self._logits else \
self._features
if self.free_log_std:
logits = self._append_free_log_std(logits)
return logits, state
@override(JAXModelV2)
def value_function(self):
assert self._features is not None, "must call forward() first"
if self._value_branch_separate:
return self._value_branch(
self._value_branch_separate(self._last_flat_in)).squeeze(1)
else:
return self._value_branch(self._features).squeeze(1)
+70
View File
@@ -0,0 +1,70 @@
import time
from ray.rllib.models.action_dist import ActionDistribution
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_jax, try_import_tfp
from ray.rllib.utils.typing import TensorType, List
jax, flax = try_import_jax()
tfp = try_import_tfp()
class JAXDistribution(ActionDistribution):
"""Wrapper class for JAX distributions."""
@override(ActionDistribution)
def __init__(self, inputs: List[TensorType], model: ModelV2):
super().__init__(inputs, model)
# Store the last sample here.
self.last_sample = None
# Use current time as pseudo-random number generator's seed.
self.prng_key = jax.random.PRNGKey(seed=int(time.time()))
@override(ActionDistribution)
def logp(self, actions: TensorType) -> TensorType:
return self.dist.log_prob(actions)
@override(ActionDistribution)
def entropy(self) -> TensorType:
return self.dist.entropy()
@override(ActionDistribution)
def kl(self, other: ActionDistribution) -> TensorType:
return self.dist.kl_divergence(other.dist)
@override(ActionDistribution)
def sample(self) -> TensorType:
# Update the state of our PRNG.
_, self.prng_key = jax.random.split(self.prng_key)
self.last_sample = jax.random.categorical(self.prng_key, self.inputs)
return self.last_sample
@override(ActionDistribution)
def sampled_action_logp(self) -> TensorType:
assert self.last_sample is not None
return self.logp(self.last_sample)
class JAXCategorical(JAXDistribution):
"""Wrapper class for a JAX Categorical distribution."""
@override(ActionDistribution)
def __init__(self, inputs, model=None, temperature=1.0):
if temperature != 1.0:
assert temperature > 0.0, \
"Categorical `temperature` must be > 0.0!"
inputs /= temperature
super().__init__(inputs, model)
self.dist = tfp.experimental.substrates.jax.distributions.Categorical(
logits=self.inputs)
@override(ActionDistribution)
def deterministic_sample(self):
self.last_sample = self.inputs.argmax(axis=1)
return self.last_sample
@staticmethod
@override(ActionDistribution)
def required_model_output_shape(action_space, model_config):
return action_space.n
+27
View File
@@ -0,0 +1,27 @@
import gym
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.utils.annotations import PublicAPI
from ray.rllib.utils.typing import ModelConfigDict
@PublicAPI
class JAXModelV2(ModelV2):
"""JAX version of ModelV2.
Note that this class by itself is not a valid model unless you
implement forward() in a subclass."""
def __init__(self, obs_space: gym.spaces.Space,
action_space: gym.spaces.Space, num_outputs: int,
model_config: ModelConfigDict, name: str):
"""Initializes a JAXModelV2 instance."""
ModelV2.__init__(
self,
obs_space,
action_space,
num_outputs,
model_config,
name,
framework="jax")
+65
View File
@@ -0,0 +1,65 @@
import time
from typing import Callable, Optional
from ray.rllib.utils.framework import get_activation_fn, try_import_jax
jax, flax = try_import_jax()
nn = np = None
if flax:
import flax.linen as nn
import jax.numpy as np
class SlimFC:
"""Simple JAX version of a fully connected layer."""
def __init__(self,
in_size,
out_size,
initializer: Optional[Callable] = None,
activation_fn: Optional[str] = None,
use_bias: bool = True,
prng_key: Optional[jax.random.PRNGKey] = None,
name: Optional[str] = None):
"""Initializes a SlimFC instance.
Args:
in_size (int): The input size of the input data that will be passed
into this layer.
out_size (int): The number of nodes in this FC layer.
initializer (flax.:
activation_fn (str): An activation string specifier, e.g. "relu".
use_bias (bool): Whether to add biases to the dot product or not.
#bias_init (float):
prng_key (Optional[jax.random.PRNGKey]): An optional PRNG key to
use for initialization. If None, create a new random one.
name (Optional[str]): An optional name for this layer.
"""
# By default, use Glorot unform initializer.
if initializer is None:
initializer = flax.nn.initializers.xavier_uniform()
self.prng_key = prng_key or jax.random.PRNGKey(int(time.time()))
_, self.prng_key = jax.random.split(self.prng_key)
# Create the flax dense layer.
self._dense = nn.Dense(
out_size,
use_bias=use_bias,
kernel_init=initializer,
name=name,
)
# Initialize it.
dummy_in = jax.random.normal(
self.prng_key, (in_size, ), dtype=np.float32)
_, self.prng_key = jax.random.split(self.prng_key)
self._params = self._dense.init(self.prng_key, dummy_in)
# Activation function (if any; default=None (linear)).
self.activation_fn = get_activation_fn(activation_fn, "jax")
def __call__(self, x):
out = self._dense.apply(self._params, x)
if self.activation_fn:
out = self.activation_fn(out)
return out
+12 -5
View File
@@ -5,6 +5,7 @@ from scipy.stats import beta, norm
import tree
import unittest
from ray.rllib.models.jax.jax_action_dist import JAXCategorical
from ray.rllib.models.tf.tf_action_dist import Beta, Categorical, \
DiagGaussian, GumbelSoftmax, MultiActionDistribution, MultiCategorical, \
SquashedGaussian
@@ -55,7 +56,9 @@ class TestDistributions(unittest.TestCase):
dist = distribution_cls(inputs, {}, **(extra_kwargs or {}))
for _ in range(100):
sample = dist.sample()
if fw != "tf":
if fw == "jax":
sample_check = sample
elif fw != "tf":
sample_check = sample.numpy()
else:
sample_check = sess.run(sample)
@@ -71,7 +74,9 @@ class TestDistributions(unittest.TestCase):
assert bounds[0] in sample_check
assert bounds[1] in sample_check
logp = dist.logp(sample)
if fw != "tf":
if fw == "jax":
logp_check = logp
elif fw != "tf":
logp_check = logp.numpy()
else:
logp_check = sess.run(logp)
@@ -88,9 +93,11 @@ class TestDistributions(unittest.TestCase):
inputs = inputs_space.sample()
for fw, sess in framework_iterator(session=True):
for fw, sess in framework_iterator(
session=True, frameworks=("jax", "tf", "tf2", "torch")):
# Create the correct distribution object.
cls = Categorical if fw != "torch" else TorchCategorical
cls = JAXCategorical if fw == "jax" else Categorical if \
fw != "torch" else TorchCategorical
categorical = cls(inputs, {})
# Do a stability test using extreme NN outputs to see whether
@@ -112,7 +119,7 @@ class TestDistributions(unittest.TestCase):
# Batch of size=3 and non-deterministic -> expect roughly the mean.
out = categorical.sample()
check(
tf.reduce_mean(out)
np.mean(out) if fw == "jax" else tf.reduce_mean(out)
if fw != "torch" else torch.mean(out.float()),
1.0,
decimals=0)
+1 -1
View File
@@ -25,7 +25,7 @@ class FullyConnectedNetwork(TorchModelV2, nn.Module):
nn.Module.__init__(self)
activation = model_config.get("fcnet_activation")
hiddens = model_config.get("fcnet_hiddens")
hiddens = model_config.get("fcnet_hiddens", [])
no_final_linear = model_config.get("no_final_linear")
self.vf_share_layers = model_config.get("vf_share_layers")
self.free_log_std = model_config.get("free_log_std")
+1 -1
View File
@@ -136,7 +136,7 @@ class SlimFC(nn.Module):
"""
super(SlimFC, self).__init__()
layers = []
# Actual Conv2D layer (including correct initialization logic).
# Actual nn.Linear layer (including correct initialization logic).
linear = nn.Linear(in_size, out_size, bias=use_bias)
if initializer:
initializer(linear.weight)
+25 -22
View File
@@ -52,29 +52,32 @@ class TestAttentionNetLearning(unittest.TestCase):
})
tune.run("PPO", config=config, stop=self.stop, verbose=1)
# TODO: (sven) causes memory failures/timeouts on Travis.
# Re-enable this once we have fast attention in master branch.
def test_impala_attention_net_learning(self):
ModelCatalog.register_custom_model("attention_net", GTrXLNet)
config = dict(
self.config, **{
"num_workers": 4,
"num_gpus": 0,
"entropy_coeff": 0.01,
"vf_loss_coeff": 0.001,
"lr": 0.0008,
"model": {
"custom_model": "attention_net",
"max_seq_len": 65,
"custom_model_config": {
"num_transformer_units": 1,
"attn_dim": 64,
"num_heads": 1,
"memory_tau": 10,
"head_dim": 32,
"ff_hidden_dim": 32,
},
},
})
tune.run("IMPALA", config=config, stop=self.stop, verbose=1)
return
# ModelCatalog.register_custom_model("attention_net", GTrXLNet)
# config = dict(
# self.config, **{
# "num_workers": 4,
# "num_gpus": 0,
# "entropy_coeff": 0.01,
# "vf_loss_coeff": 0.001,
# "lr": 0.0008,
# "model": {
# "custom_model": "attention_net",
# "max_seq_len": 65,
# "custom_model_config": {
# "num_transformer_units": 1,
# "attn_dim": 64,
# "num_heads": 1,
# "memory_tau": 10,
# "head_dim": 32,
# "ff_hidden_dim": 32,
# },
# },
# })
# tune.run("IMPALA", config=config, stop=self.stop, verbose=1)
if __name__ == "__main__":
+30 -15
View File
@@ -9,12 +9,12 @@ from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.tf.tf_action_dist import TFActionDistribution
from ray.rllib.models.preprocessors import (NoPreprocessor, OneHotPreprocessor,
Preprocessor)
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork
from ray.rllib.models.tf.visionnet import VisionNetwork
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf
from ray.rllib.utils.framework import try_import_tf, try_import_torch
from ray.rllib.utils.test_utils import framework_iterator
tf1, tf, tfv = try_import_tf()
torch, _ = try_import_torch()
class CustomPreprocessor(Preprocessor):
@@ -102,19 +102,34 @@ class TestModelCatalog(unittest.TestCase):
def test_default_models(self):
ray.init(object_store_memory=1000 * 1024 * 1024)
p1 = ModelCatalog.get_model_v2(
obs_space=Box(0, 1, shape=(3, ), dtype=np.float32),
action_space=Discrete(5),
num_outputs=5,
model_config={})
self.assertEqual(type(p1), FullyConnectedNetwork)
for fw in framework_iterator(frameworks=("jax", "tf", "tf2", "torch")):
obs_space = Box(0, 1, shape=(3, ), dtype=np.float32)
p1 = ModelCatalog.get_model_v2(
obs_space=obs_space,
action_space=Discrete(5),
num_outputs=5,
model_config={},
framework=fw,
)
self.assertTrue("FullyConnectedNetwork" in type(p1).__name__)
# Do a test forward pass.
obs = np.array([obs_space.sample()])
if fw == "torch":
obs = torch.from_numpy(obs)
out, state_outs = p1({"obs": obs})
self.assertTrue(out.shape == (1, 5))
self.assertTrue(state_outs == [])
p2 = ModelCatalog.get_model_v2(
obs_space=Box(0, 1, shape=(84, 84, 3), dtype=np.float32),
action_space=Discrete(5),
num_outputs=5,
model_config={})
self.assertEqual(type(p2), VisionNetwork)
# No Conv2Ds for JAX yet.
if fw != "jax":
p2 = ModelCatalog.get_model_v2(
obs_space=Box(0, 1, shape=(84, 84, 3), dtype=np.float32),
action_space=Discrete(5),
num_outputs=5,
model_config={},
framework=fw,
)
self.assertTrue("VisionNetwork" in type(p2).__name__)
def test_custom_model(self):
ray.init(object_store_memory=1000 * 1024 * 1024)
+37
View File
@@ -15,6 +15,33 @@ TensorType = TensorType
TensorStructType = TensorStructType
def try_import_jax(error=False):
"""Tries importing JAX and returns the module (or None).
Args:
error (bool): Whether to raise an error if JAX cannot be imported.
Returns:
The jax module.
Raises:
ImportError: If error=True and JAX is not installed.
"""
if "RLLIB_TEST_NO_JAX_IMPORT" in os.environ:
logger.warning("Not importing JAX for test purposes.")
return None
try:
import jax
import flax
except ImportError as e:
if error:
raise e
return None, None
return jax, flax
def try_import_tf(error=False):
"""Tries importing tf and returns the module (or None).
@@ -251,6 +278,16 @@ def get_activation_fn(name: Optional[str] = None, framework: str = "tf"):
return nn.ReLU
elif name == "tanh":
return nn.Tanh
elif framework == "jax":
if name in ["linear", None]:
return None
jax = try_import_jax()
if name == "swish":
return jax.nn.swish
if name == "relu":
return jax.nn.relu
elif name == "tanh":
return jax.nn.hard_tanh
else:
if name in ["linear", None]:
return None
+7 -2
View File
@@ -2,8 +2,10 @@ import gym
import logging
import numpy as np
from ray.rllib.utils.framework import try_import_tf, try_import_torch
from ray.rllib.utils.framework import try_import_jax, try_import_tf, \
try_import_torch
jax = try_import_jax()
tf1, tf, tfv = try_import_tf()
if tf1:
eager_mode = None
@@ -65,7 +67,10 @@ def framework_iterator(config=None,
logger.warning(
"framework_iterator skipping tf2.x (tf version is < 2.0)!")
continue
assert fw in ["tf2", "tf", "tfe", "torch", None]
elif fw == "jax" and not jax:
logger.warning("framework_iterator skipping JAX (not installed)!")
continue
assert fw in ["tf2", "tf", "tfe", "torch", "jax", None]
# Do we need a test session?
sess = None