Lim0011's picture
Upload 251 files
85e3d20 verified
# Copyright 2021 DeepMind Technologies Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""JAX implementation of CLRS baseline models."""
import functools
import os
import pickle
from typing import Dict, List, Optional, Tuple, Union
import chex
from clrs._src import decoders
from clrs._src import losses
from clrs._src import model
from clrs._src import nets
from clrs._src import probing
from clrs._src import processors
from clrs._src import samplers
from clrs._src import specs
import haiku as hk
import jax
import jax.numpy as jnp
import numpy as np
import optax
_Array = chex.Array
_DataPoint = probing.DataPoint
_Features = samplers.Features
_FeaturesChunked = samplers.FeaturesChunked
_Feedback = samplers.Feedback
_Location = specs.Location
_Seed = jnp.integer
_Spec = specs.Spec
_Stage = specs.Stage
_Trajectory = samplers.Trajectory
_Type = specs.Type
_OutputClass = specs.OutputClass
# pytype: disable=signature-mismatch
def _maybe_pick_first_pmapped(tree):
if jax.local_device_count() == 1:
return tree
return jax.tree_util.tree_map(lambda x: x[0], tree)
@jax.jit
def _restack_from_pmap(tree):
"""Stack the results of a pmapped computation across the first two axes."""
restack_array = lambda x: jnp.reshape(x, (-1,) + x.shape[2:])
return jax.tree_util.tree_map(restack_array, tree)
def _maybe_restack_from_pmap(tree):
if jax.local_device_count() == 1:
return tree
return _restack_from_pmap(tree)
@functools.partial(jax.jit, static_argnums=[1, 2])
def _pmap_reshape(x, n_devices, split_axis=0):
"""Splits a pytree over n_devices on axis split_axis for pmapping."""
def _reshape(arr):
new_shape = (arr.shape[:split_axis] +
(n_devices, arr.shape[split_axis] // n_devices) +
arr.shape[split_axis + 1:])
return jnp.moveaxis(jnp.reshape(arr, new_shape), split_axis, 0)
return jax.tree_util.tree_map(_reshape, x)
def _maybe_pmap_reshape(x, split_axis=0):
n_devices = jax.local_device_count()
if n_devices == 1:
return x
return _pmap_reshape(x, n_devices, split_axis)
@functools.partial(jax.jit, static_argnums=1)
def _pmap_data(data: Union[_Feedback, _Features], n_devices: int):
"""Replicate/split feedback or features for pmapping."""
if isinstance(data, _Feedback):
features = data.features
else:
features = data
pmap_data = features._replace(
inputs=_pmap_reshape(features.inputs, n_devices),
hints=_pmap_reshape(features.hints, n_devices, split_axis=1),
lengths=_pmap_reshape(features.lengths, n_devices),
)
if isinstance(data, _Feedback):
pmap_data = data._replace(
features=pmap_data,
outputs=_pmap_reshape(data.outputs, n_devices)
)
return pmap_data
def _maybe_pmap_data(data: Union[_Feedback, _Features]):
n_devices = jax.local_device_count()
if n_devices == 1:
return data
return _pmap_data(data, n_devices)
def _maybe_put_replicated(tree):
if jax.local_device_count() == 1:
return jax.device_put(tree)
else:
return jax.device_put_replicated(tree, jax.local_devices())
def _maybe_pmap_rng_key(rng_key: _Array):
n_devices = jax.local_device_count()
if n_devices == 1:
return rng_key
pmap_rng_keys = jax.random.split(rng_key, n_devices)
return jax.device_put_sharded(list(pmap_rng_keys), jax.local_devices())
class BaselineModel(model.Model):
"""Model implementation with selectable message passing algorithm."""
def __init__(
self,
spec: Union[_Spec, List[_Spec]],
dummy_trajectory: Union[List[_Feedback], _Feedback],
processor_factory: processors.ProcessorFactory,
hidden_dim: int = 32,
encode_hints: bool = False,
decode_hints: bool = True,
encoder_init: str = 'default',
use_lstm: bool = False,
learning_rate: float = 0.005,
grad_clip_max_norm: float = 0.0,
checkpoint_path: str = '/tmp/clrs3',
freeze_processor: bool = False,
dropout_prob: float = 0.0,
hint_teacher_forcing: float = 0.0,
hint_repred_mode: str = 'soft',
name: str = 'base_model',
nb_msg_passing_steps: int = 1,
):
"""Constructor for BaselineModel.
The model consists of encoders, processor and decoders. It can train
and evaluate either a single algorithm or a set of algorithms; in the
latter case, a single processor is shared among all the algorithms, while
the encoders and decoders are separate for each algorithm.
Args:
spec: Either a single spec for one algorithm, or a list of specs for
multiple algorithms to be trained and evaluated.
dummy_trajectory: Either a single feedback batch, in the single-algorithm
case, or a list of feedback batches, in the multi-algorithm case, that
comply with the `spec` (or list of specs), to initialize network size.
processor_factory: A callable that takes an `out_size` parameter
and returns a processor (see `processors.py`).
hidden_dim: Size of the hidden state of the model, i.e., size of the
message-passing vectors.
encode_hints: Whether to provide hints as model inputs.
decode_hints: Whether to provide hints as model outputs.
encoder_init: The initialiser type to use for the encoders.
use_lstm: Whether to insert an LSTM after message passing.
learning_rate: Learning rate for training.
grad_clip_max_norm: if greater than 0, the maximum norm of the gradients.
checkpoint_path: Path for loading/saving checkpoints.
freeze_processor: If True, the processor weights will be frozen and
only encoders and decoders (and, if used, the lstm) will be trained.
dropout_prob: Dropout rate in the message-passing stage.
hint_teacher_forcing: Probability of using ground-truth hints instead
of predicted hints as inputs during training (only relevant if
`encode_hints`=True)
hint_repred_mode: How to process predicted hints when fed back as inputs.
Only meaningful when `encode_hints` and `decode_hints` are True.
Options are:
- 'soft', where we use softmaxes for categoricals, pointers
and mask_one, and sigmoids for masks. This will allow gradients
to flow through hints during training.
- 'hard', where we use argmax instead of softmax, and hard
thresholding of masks. No gradients will go through the hints
during training; even for scalar hints, which don't have any
kind of post-processing, gradients will be stopped.
- 'hard_on_eval', which is soft for training and hard for evaluation.
name: Model name.
nb_msg_passing_steps: Number of message passing steps per hint.
Raises:
ValueError: if `encode_hints=True` and `decode_hints=False`.
"""
super(BaselineModel, self).__init__(spec=spec)
if encode_hints and not decode_hints:
raise ValueError('`encode_hints=True`, `decode_hints=False` is invalid.')
assert hint_repred_mode in ['soft', 'hard', 'hard_on_eval']
self.decode_hints = decode_hints
self.checkpoint_path = checkpoint_path
self.name = name
self._freeze_processor = freeze_processor
if grad_clip_max_norm != 0.0:
optax_chain = [optax.clip_by_global_norm(grad_clip_max_norm),
optax.scale_by_adam(),
optax.scale(-learning_rate)]
self.opt = optax.chain(*optax_chain)
else:
self.opt = optax.adam(learning_rate)
self.nb_msg_passing_steps = nb_msg_passing_steps
self.nb_dims = []
if isinstance(dummy_trajectory, _Feedback):
assert len(self._spec) == 1
dummy_trajectory = [dummy_trajectory]
for traj in dummy_trajectory:
nb_dims = {}
for inp in traj.features.inputs:
nb_dims[inp.name] = inp.data.shape[-1]
for hint in traj.features.hints:
nb_dims[hint.name] = hint.data.shape[-1]
for outp in traj.outputs:
nb_dims[outp.name] = outp.data.shape[-1]
self.nb_dims.append(nb_dims)
self._create_net_fns(hidden_dim, encode_hints, processor_factory, use_lstm,
encoder_init, dropout_prob, hint_teacher_forcing,
hint_repred_mode)
self._device_params = None
self._device_opt_state = None
self.opt_state_skeleton = None
def _create_net_fns(self, hidden_dim, encode_hints, processor_factory,
use_lstm, encoder_init, dropout_prob,
hint_teacher_forcing, hint_repred_mode):
def _use_net(*args, **kwargs):
return nets.Net(self._spec, hidden_dim, encode_hints, self.decode_hints,
processor_factory, use_lstm, encoder_init,
dropout_prob, hint_teacher_forcing,
hint_repred_mode,
self.nb_dims, self.nb_msg_passing_steps)(*args, **kwargs)
self.net_fn = hk.transform(_use_net)
pmap_args = dict(axis_name='batch', devices=jax.local_devices())
n_devices = jax.local_device_count()
func, static_arg, extra_args = (
(jax.jit, 'static_argnums', {}) if n_devices == 1 else
(jax.pmap, 'static_broadcasted_argnums', pmap_args))
pmean = functools.partial(jax.lax.pmean, axis_name='batch')
self._maybe_pmean = pmean if n_devices > 1 else lambda x: x
extra_args[static_arg] = 3
self.jitted_grad = func(self._compute_grad, **extra_args)
extra_args[static_arg] = 4
self.jitted_feedback = func(self._feedback, donate_argnums=[0, 3],
**extra_args)
extra_args[static_arg] = [3, 4, 5]
self.jitted_predict = func(self._predict, **extra_args)
extra_args[static_arg] = [3, 4]
self.jitted_accum_opt_update = func(accum_opt_update, donate_argnums=[0, 2],
**extra_args)
def init(self, features: Union[_Features, List[_Features]], seed: _Seed):
if not isinstance(features, list):
assert len(self._spec) == 1
features = [features]
self.params = self.net_fn.init(jax.random.PRNGKey(seed), features, True, # pytype: disable=wrong-arg-types # jax-ndarray
algorithm_index=-1,
return_hints=False,
return_all_outputs=False)
self.opt_state = self.opt.init(self.params)
# We will use the optimizer state skeleton for traversal when we
# want to avoid updating the state of params of untrained algorithms.
self.opt_state_skeleton = self.opt.init(jnp.zeros(1))
@property
def params(self):
if self._device_params is None:
return None
return jax.device_get(_maybe_pick_first_pmapped(self._device_params))
@params.setter
def params(self, params):
self._device_params = _maybe_put_replicated(params)
@property
def opt_state(self):
if self._device_opt_state is None:
return None
return jax.device_get(_maybe_pick_first_pmapped(self._device_opt_state))
@opt_state.setter
def opt_state(self, opt_state):
self._device_opt_state = _maybe_put_replicated(opt_state)
def _compute_grad(self, params, rng_key, feedback, algorithm_index):
lss, grads = jax.value_and_grad(self._loss)(
params, rng_key, feedback, algorithm_index)
return self._maybe_pmean(lss), self._maybe_pmean(grads)
def _feedback(self, params, rng_key, feedback, opt_state, algorithm_index):
lss, grads = jax.value_and_grad(self._loss)(
params, rng_key, feedback, algorithm_index)
grads = self._maybe_pmean(grads)
params, opt_state = self._update_params(params, grads, opt_state,
algorithm_index)
lss = self._maybe_pmean(lss)
return lss, params, opt_state
def _predict(self, params, rng_key: hk.PRNGSequence, features: _Features,
algorithm_index: int, return_hints: bool,
return_all_outputs: bool):
outs, hint_preds = self.net_fn.apply(
params, rng_key, [features],
repred=True, algorithm_index=algorithm_index,
return_hints=return_hints,
return_all_outputs=return_all_outputs)
outs = decoders.postprocess(self._spec[algorithm_index],
outs,
sinkhorn_temperature=0.1,
sinkhorn_steps=50,
hard=True,
)
return outs, hint_preds
def compute_grad(
self,
rng_key: hk.PRNGSequence,
feedback: _Feedback,
algorithm_index: Optional[int] = None,
) -> Tuple[float, _Array]:
"""Compute gradients."""
if algorithm_index is None:
assert len(self._spec) == 1
algorithm_index = 0
assert algorithm_index >= 0
# Calculate gradients.
rng_keys = _maybe_pmap_rng_key(rng_key) # pytype: disable=wrong-arg-types # numpy-scalars
feedback = _maybe_pmap_data(feedback)
loss, grads = self.jitted_grad(
self._device_params, rng_keys, feedback, algorithm_index)
loss = _maybe_pick_first_pmapped(loss)
grads = _maybe_pick_first_pmapped(grads)
return loss, grads
def feedback(self, rng_key: hk.PRNGSequence, feedback: _Feedback,
algorithm_index=None) -> float:
if algorithm_index is None:
assert len(self._spec) == 1
algorithm_index = 0
# Calculate and apply gradients.
rng_keys = _maybe_pmap_rng_key(rng_key) # pytype: disable=wrong-arg-types # numpy-scalars
feedback = _maybe_pmap_data(feedback)
loss, self._device_params, self._device_opt_state = self.jitted_feedback(
self._device_params, rng_keys, feedback,
self._device_opt_state, algorithm_index)
loss = _maybe_pick_first_pmapped(loss)
return loss
def predict(self, rng_key: hk.PRNGSequence, features: _Features,
algorithm_index: Optional[int] = None,
return_hints: bool = False,
return_all_outputs: bool = False):
"""Model inference step."""
if algorithm_index is None:
assert len(self._spec) == 1
algorithm_index = 0
rng_keys = _maybe_pmap_rng_key(rng_key) # pytype: disable=wrong-arg-types # numpy-scalars
features = _maybe_pmap_data(features)
return _maybe_restack_from_pmap(
self.jitted_predict(
self._device_params, rng_keys, features,
algorithm_index,
return_hints,
return_all_outputs))
def _loss(self, params, rng_key, feedback, algorithm_index):
"""Calculates model loss f(feedback; params)."""
output_preds, hint_preds = self.net_fn.apply(
params, rng_key, [feedback.features],
repred=False,
algorithm_index=algorithm_index,
return_hints=True,
return_all_outputs=False)
nb_nodes = _nb_nodes(feedback, is_chunked=False)
lengths = feedback.features.lengths
total_loss = 0.0
# Calculate output loss.
for truth in feedback.outputs:
total_loss += losses.output_loss(
truth=truth,
pred=output_preds[truth.name],
nb_nodes=nb_nodes,
)
# Optionally accumulate hint losses.
if self.decode_hints:
for truth in feedback.features.hints:
total_loss += losses.hint_loss(
truth=truth,
preds=[x[truth.name] for x in hint_preds],
lengths=lengths,
nb_nodes=nb_nodes,
)
return total_loss
def _update_params(self, params, grads, opt_state, algorithm_index):
updates, opt_state = filter_null_grads(
grads, self.opt, opt_state, self.opt_state_skeleton, algorithm_index)
if self._freeze_processor:
params_subset = _filter_out_processor(params)
updates_subset = _filter_out_processor(updates)
assert len(params) > len(params_subset)
assert params_subset
new_params = optax.apply_updates(params_subset, updates_subset)
new_params = hk.data_structures.merge(params, new_params)
else:
new_params = optax.apply_updates(params, updates)
return new_params, opt_state
def update_model_params_accum(self, grads) -> None:
grads = _maybe_put_replicated(grads)
self._device_params, self._device_opt_state = self.jitted_accum_opt_update(
self._device_params, grads, self._device_opt_state, self.opt,
self._freeze_processor)
def verbose_loss(self, feedback: _Feedback, extra_info) -> Dict[str, _Array]:
"""Gets verbose loss information."""
hint_preds = extra_info
nb_nodes = _nb_nodes(feedback, is_chunked=False)
lengths = feedback.features.lengths
losses_ = {}
# Optionally accumulate hint losses.
if self.decode_hints:
for truth in feedback.features.hints:
losses_.update(
losses.hint_loss(
truth=truth,
preds=[x[truth.name] for x in hint_preds],
lengths=lengths,
nb_nodes=nb_nodes,
verbose=True,
))
return losses_
def restore_model(self, file_name: str, only_load_processor: bool = False):
"""Restore model from `file_name`."""
path = os.path.join(self.checkpoint_path, file_name)
with open(path, 'rb') as f:
restored_state = pickle.load(f)
if only_load_processor:
restored_params = _filter_in_processor(restored_state['params'])
else:
restored_params = restored_state['params']
self.params = hk.data_structures.merge(self.params, restored_params)
self.opt_state = restored_state['opt_state']
def save_model(self, file_name: str):
"""Save model (processor weights only) to `file_name`."""
os.makedirs(self.checkpoint_path, exist_ok=True)
to_save = {'params': self.params, 'opt_state': self.opt_state}
path = os.path.join(self.checkpoint_path, file_name)
with open(path, 'wb') as f:
pickle.dump(to_save, f)
class BaselineModelChunked(BaselineModel):
"""Model that processes time-chunked data.
Unlike `BaselineModel`, which processes full samples, `BaselineModelChunked`
processes fixed-timelength chunks of data. Each tensor of inputs and hints
has dimensions chunk_length x batch_size x ... The beginning of a new
sample withing the chunk is signalled by a tensor called `is_first` of
dimensions chunk_length x batch_size.
The chunked model is intended for training. For validation and test, use
`BaselineModel`.
"""
mp_states: List[List[nets.MessagePassingStateChunked]]
init_mp_states: List[List[nets.MessagePassingStateChunked]]
def _create_net_fns(self, hidden_dim, encode_hints, processor_factory,
use_lstm, encoder_init, dropout_prob,
hint_teacher_forcing, hint_repred_mode):
def _use_net(*args, **kwargs):
return nets.NetChunked(
self._spec, hidden_dim, encode_hints, self.decode_hints,
processor_factory, use_lstm, encoder_init, dropout_prob,
hint_teacher_forcing, hint_repred_mode,
self.nb_dims, self.nb_msg_passing_steps)(*args, **kwargs)
self.net_fn = hk.transform(_use_net)
pmap_args = dict(axis_name='batch', devices=jax.local_devices())
n_devices = jax.local_device_count()
func, static_arg, extra_args = (
(jax.jit, 'static_argnums', {}) if n_devices == 1 else
(jax.pmap, 'static_broadcasted_argnums', pmap_args))
pmean = functools.partial(jax.lax.pmean, axis_name='batch')
self._maybe_pmean = pmean if n_devices > 1 else lambda x: x
extra_args[static_arg] = 4
self.jitted_grad = func(self._compute_grad, **extra_args)
extra_args[static_arg] = 5
self.jitted_feedback = func(self._feedback, donate_argnums=[0, 4],
**extra_args)
extra_args[static_arg] = [3, 4]
self.jitted_accum_opt_update = func(accum_opt_update, donate_argnums=[0, 2],
**extra_args)
def _init_mp_state(self, features_list: List[List[_FeaturesChunked]],
rng_key: _Array):
def _empty_mp_state():
return nets.MessagePassingStateChunked( # pytype: disable=wrong-arg-types # numpy-scalars
inputs=None, hints=None, is_first=None,
hint_preds=None, hiddens=None, lstm_state=None)
empty_mp_states = [[_empty_mp_state() for _ in f] for f in features_list]
dummy_params = [self.net_fn.init(rng_key, f, e, False,
init_mp_state=True, algorithm_index=-1)
for (f, e) in zip(features_list, empty_mp_states)]
mp_states = [
self.net_fn.apply(d, rng_key, f, e, False,
init_mp_state=True, algorithm_index=-1)[1]
for (d, f, e) in zip(dummy_params, features_list, empty_mp_states)]
return mp_states
def init(self,
features: List[List[_FeaturesChunked]],
seed: _Seed):
self.mp_states = self._init_mp_state(features,
jax.random.PRNGKey(seed)) # pytype: disable=wrong-arg-types # jax-ndarray
self.init_mp_states = [list(x) for x in self.mp_states]
self.params = self.net_fn.init(
jax.random.PRNGKey(seed), features[0], self.mp_states[0], # pytype: disable=wrong-arg-types # jax-ndarray
True, init_mp_state=False, algorithm_index=-1)
self.opt_state = self.opt.init(self.params)
# We will use the optimizer state skeleton for traversal when we
# want to avoid updating the state of params of untrained algorithms.
self.opt_state_skeleton = self.opt.init(jnp.zeros(1))
def predict(self, rng_key: hk.PRNGSequence, features: _FeaturesChunked,
algorithm_index: Optional[int] = None):
"""Inference not implemented. Chunked model intended for training only."""
raise NotImplementedError
def _loss(self, params, rng_key, feedback, mp_state, algorithm_index):
(output_preds, hint_preds), mp_state = self.net_fn.apply(
params, rng_key, [feedback.features],
[mp_state],
repred=False,
init_mp_state=False,
algorithm_index=algorithm_index)
nb_nodes = _nb_nodes(feedback, is_chunked=True)
total_loss = 0.0
is_first = feedback.features.is_first
is_last = feedback.features.is_last
# Calculate output loss.
for truth in feedback.outputs:
total_loss += losses.output_loss_chunked(
truth=truth,
pred=output_preds[truth.name],
is_last=is_last,
nb_nodes=nb_nodes,
)
# Optionally accumulate hint losses.
if self.decode_hints:
for truth in feedback.features.hints:
loss = losses.hint_loss_chunked(
truth=truth,
pred=hint_preds[truth.name],
is_first=is_first,
nb_nodes=nb_nodes,
)
total_loss += loss
return total_loss, (mp_state,)
def _compute_grad(self, params, rng_key, feedback, mp_state, algorithm_index):
(lss, (mp_state,)), grads = jax.value_and_grad(self._loss, has_aux=True)(
params, rng_key, feedback, mp_state, algorithm_index)
return self._maybe_pmean(lss), mp_state, self._maybe_pmean(grads)
def _feedback(self, params, rng_key, feedback, mp_state, opt_state,
algorithm_index):
(lss, (mp_state,)), grads = jax.value_and_grad(self._loss, has_aux=True)(
params, rng_key, feedback, mp_state, algorithm_index)
grads = self._maybe_pmean(grads)
params, opt_state = self._update_params(params, grads, opt_state,
algorithm_index)
lss = self._maybe_pmean(lss)
return lss, params, opt_state, mp_state
def compute_grad(
self,
rng_key: hk.PRNGSequence,
feedback: _Feedback,
algorithm_index: Optional[Tuple[int, int]] = None,
) -> Tuple[float, _Array]:
"""Compute gradients."""
if algorithm_index is None:
assert len(self._spec) == 1
algorithm_index = (0, 0)
length_index, algorithm_index = algorithm_index
# Reusing init_mp_state improves performance.
# The next, commented out line, should be used for proper state keeping.
# mp_state = self.mp_states[length_index][algorithm_index]
mp_state = self.init_mp_states[length_index][algorithm_index]
rng_keys = _maybe_pmap_rng_key(rng_key) # pytype: disable=wrong-arg-types # numpy-scalars
feedback = _maybe_pmap_reshape(feedback, split_axis=1)
mp_state = _maybe_pmap_reshape(mp_state, split_axis=0)
loss, mp_state, grads = self.jitted_grad(
self._device_params, rng_keys, feedback, mp_state, algorithm_index)
loss = _maybe_pick_first_pmapped(loss)
grads = _maybe_pick_first_pmapped(grads)
mp_state = _maybe_restack_from_pmap(mp_state)
self.mp_states[length_index][algorithm_index] = mp_state
return loss, grads
def feedback(self, rng_key: hk.PRNGSequence, feedback: _Feedback,
algorithm_index=None) -> float:
if algorithm_index is None:
assert len(self._spec) == 1
algorithm_index = (0, 0)
length_index, algorithm_index = algorithm_index
# Reusing init_mp_state improves performance.
# The next, commented out line, should be used for proper state keeping.
# mp_state = self.mp_states[length_index][algorithm_index]
mp_state = self.init_mp_states[length_index][algorithm_index]
rng_keys = _maybe_pmap_rng_key(rng_key) # pytype: disable=wrong-arg-types # numpy-scalars
feedback = _maybe_pmap_reshape(feedback, split_axis=1)
mp_state = _maybe_pmap_reshape(mp_state, split_axis=0)
loss, self._device_params, self._device_opt_state, mp_state = (
self.jitted_feedback(
self._device_params, rng_keys, feedback,
mp_state, self._device_opt_state, algorithm_index))
loss = _maybe_pick_first_pmapped(loss)
mp_state = _maybe_restack_from_pmap(mp_state)
self.mp_states[length_index][algorithm_index] = mp_state
return loss
def verbose_loss(self, *args, **kwargs):
raise NotImplementedError
def _nb_nodes(feedback: _Feedback, is_chunked) -> int:
for inp in feedback.features.inputs:
if inp.location in [_Location.NODE, _Location.EDGE]:
if is_chunked:
return inp.data.shape[2] # inputs are time x batch x nodes x ...
else:
return inp.data.shape[1] # inputs are batch x nodes x ...
assert False
def _param_in_processor(module_name):
return processors.PROCESSOR_TAG in module_name
def _filter_out_processor(params: hk.Params) -> hk.Params:
return hk.data_structures.filter(
lambda module_name, n, v: not _param_in_processor(module_name), params)
def _filter_in_processor(params: hk.Params) -> hk.Params:
return hk.data_structures.filter(
lambda module_name, n, v: _param_in_processor(module_name), params)
def _is_not_done_broadcast(lengths, i, tensor):
is_not_done = (lengths > i + 1) * 1.0
while len(is_not_done.shape) < len(tensor.shape):
is_not_done = jnp.expand_dims(is_not_done, -1)
return is_not_done
def accum_opt_update(params, grads, opt_state, opt, freeze_processor):
"""Update params from gradients collected from several algorithms."""
# Average the gradients over all algos
grads = jax.tree_util.tree_map(
lambda *x: sum(x) / (sum([jnp.any(k) for k in x]) + 1e-12), *grads)
updates, opt_state = opt.update(grads, opt_state)
if freeze_processor:
params_subset = _filter_out_processor(params)
assert len(params) > len(params_subset)
assert params_subset
updates_subset = _filter_out_processor(updates)
new_params = optax.apply_updates(params_subset, updates_subset)
new_params = hk.data_structures.merge(params, new_params)
else:
new_params = optax.apply_updates(params, updates)
return new_params, opt_state
@functools.partial(jax.jit, static_argnames=['opt'])
def opt_update(opt, flat_grads, flat_opt_state):
return opt.update(flat_grads, flat_opt_state)
def filter_null_grads(grads, opt, opt_state, opt_state_skeleton, algo_idx):
"""Compute updates ignoring params that have no gradients.
This prevents untrained params (e.g., encoders/decoders for algorithms
that are not being trained) to accumulate, e.g., momentum from spurious
zero gradients.
Note: this works as intended for "per-parameter" optimizer state, such as
momentum. However, when the optimizer has some global state (such as the
step counts in Adam), the global state will be updated every time,
affecting also future updates of parameters that had null gradients in the
current step.
Args:
grads: Gradients for all parameters.
opt: Optax optimizer.
opt_state: Optimizer state.
opt_state_skeleton: A "skeleton" of optimizer state that has been
initialized with scalar parameters. This serves to traverse each parameter
of the otpimizer state during the opt state update.
algo_idx: Index of algorithm, to filter out unused encoders/decoders.
If None, no filtering happens.
Returns:
Updates and new optimizer state, where the parameters with null gradient
have not been taken into account.
"""
def _keep_in_algo(k, v):
"""Ignore params of encoders/decoders irrelevant for this algo."""
# Note: in shared pointer decoder modes, we should exclude shared params
# for algos that do not have pointer outputs.
if ((processors.PROCESSOR_TAG in k) or
(f'algo_{algo_idx}_' in k)):
return v
return jax.tree_util.tree_map(lambda x: None, v)
if algo_idx is None:
masked_grads = grads
else:
masked_grads = {k: _keep_in_algo(k, v) for k, v in grads.items()}
flat_grads, treedef = jax.tree_util.tree_flatten(masked_grads)
flat_opt_state = jax.tree_util.tree_map(
lambda _, x: x # pylint:disable=g-long-lambda
if isinstance(x, (np.ndarray, jax.Array))
else treedef.flatten_up_to(x),
opt_state_skeleton,
opt_state,
)
# Compute updates only for the params with gradient.
flat_updates, flat_opt_state = opt_update(opt, flat_grads, flat_opt_state)
def unflatten(flat, original):
"""Restore tree structure, filling missing (None) leaves with original."""
if isinstance(flat, (np.ndarray, jax.Array)):
return flat
return jax.tree_util.tree_map(lambda x, y: x if y is None else y, original,
treedef.unflatten(flat))
# Restore the state and updates tree structure.
new_opt_state = jax.tree_util.tree_map(lambda _, x, y: unflatten(x, y),
opt_state_skeleton, flat_opt_state,
opt_state)
updates = unflatten(flat_updates,
jax.tree_util.tree_map(lambda x: 0., grads))
return updates, new_opt_state