File size: 31,361 Bytes
85e3d20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# 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