1995 lines
77 KiB
Python
1995 lines
77 KiB
Python
# Copyright 2018 The TensorFlow Authors. 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.
|
|
# ==============================================================================
|
|
"""Various classes representing distributed inputs."""
|
|
|
|
import functools
|
|
import sys
|
|
import time
|
|
|
|
import six
|
|
|
|
from tensorflow.python.autograph.operators import py_builtins
|
|
from tensorflow.python.data.experimental.ops import batching
|
|
from tensorflow.python.data.experimental.ops import cardinality as cardinality_lib
|
|
from tensorflow.python.data.experimental.ops import distribute
|
|
from tensorflow.python.data.ops import dataset_ops
|
|
from tensorflow.python.data.ops import iterator_ops
|
|
from tensorflow.python.data.ops import multi_device_iterator_ops
|
|
from tensorflow.python.data.ops import optional_ops
|
|
from tensorflow.python.distribute import device_util
|
|
from tensorflow.python.distribute import distribute_lib
|
|
from tensorflow.python.distribute import distribute_utils
|
|
from tensorflow.python.distribute import input_ops
|
|
from tensorflow.python.distribute import reduce_util
|
|
from tensorflow.python.distribute import values
|
|
from tensorflow.python.distribute.distribute_lib import InputReplicationMode
|
|
from tensorflow.python.eager import context
|
|
from tensorflow.python.eager import monitoring
|
|
from tensorflow.python.framework import composite_tensor
|
|
from tensorflow.python.framework import device as tf_device
|
|
from tensorflow.python.framework import dtypes
|
|
from tensorflow.python.framework import errors
|
|
from tensorflow.python.framework import ops
|
|
from tensorflow.python.framework import sparse_tensor
|
|
from tensorflow.python.framework import tensor_shape
|
|
from tensorflow.python.framework import tensor_util
|
|
from tensorflow.python.framework import type_spec
|
|
from tensorflow.python.ops import array_ops
|
|
from tensorflow.python.ops import cond as tf_cond
|
|
from tensorflow.python.ops import math_ops
|
|
from tensorflow.python.ops import while_loop
|
|
from tensorflow.python.ops.ragged import ragged_tensor
|
|
from tensorflow.python.platform import tf_logging as logging
|
|
from tensorflow.python.types import distribute as distribute_types
|
|
from tensorflow.python.util import nest
|
|
from tensorflow.python.util.compat import collections_abc
|
|
|
|
|
|
_distributed_dataset_initialization_time_milliseconds = monitoring.Sampler(
|
|
"/tensorflow/api/distribution_strategy/"
|
|
"distributed_dataset_initialization_time_milliseconds",
|
|
monitoring.ExponentialBuckets(scale=1, growth_factor=2, bucket_count=26),
|
|
"Track the time (in milliseconds) to initialize distributed datasets.",
|
|
"strategy", "workers")
|
|
|
|
_distributed_dataset_from_function_initialization_time_milliseconds = (
|
|
monitoring.Sampler(
|
|
"/tensorflow/api/distribution_strategy/"
|
|
"distributed_dataset_from_function_initialization_time_milliseconds",
|
|
monitoring.ExponentialBuckets(
|
|
scale=1, growth_factor=2, bucket_count=26),
|
|
"Track the time (in milliseconds) to initialize distributed datasets "
|
|
"from function.",
|
|
"strategy", "workers"))
|
|
|
|
|
|
def get_iterator_spec_from_dataset(strategy, dataset):
|
|
"""Returns an iterator spec from dataset function.
|
|
|
|
This function constructs type spec for iterator obtained from
|
|
iter(dataset).
|
|
|
|
Args:
|
|
strategy: a `tf.distribute.Strategy` object, used to run all-reduce to
|
|
handle last partial batch.
|
|
dataset: A tf.data.Dataset instance. If using a function that returns a
|
|
tf.data.Dataset instance, pass dataset_fn.structured_outputs.
|
|
|
|
Returns:
|
|
A type_spec for iterator for dataset instance.
|
|
|
|
"""
|
|
# pylint: disable=protected-access
|
|
output_element_spec = dataset.element_spec
|
|
if isinstance(dataset._type_spec,
|
|
(DistributedDatasetSpec,
|
|
DistributedDatasetsFromFunctionSpec)):
|
|
iterator_type_spec = DistributedIteratorSpec(
|
|
strategy.extended._input_workers_with_options(),
|
|
output_element_spec,
|
|
strategy.extended._container_strategy(),
|
|
options=None,
|
|
cardinality=dataset.cardinality,
|
|
enable_get_next_as_optional=True)
|
|
else:
|
|
if strategy.extended._num_gpus_per_worker:
|
|
logging.warning(
|
|
f"{strategy.extended._num_gpus_per_worker} GPUs "
|
|
"are allocated per worker. Please use DistributedDataset by "
|
|
"calling strategy.experimental_distribute_dataset or strategy."
|
|
"distribute_datasets_from_function to make best use of GPU "
|
|
"resources"
|
|
)
|
|
iterator_type_spec = iterator_ops.IteratorSpec(output_element_spec)
|
|
return iterator_type_spec
|
|
# pylint: enable=protected-access
|
|
|
|
|
|
class InputWorkers(object):
|
|
"""A 1-to-many mapping from input worker devices to compute devices."""
|
|
|
|
# TODO(ishark): Remove option canonicalize_devices and make all the callers
|
|
# pass canonicalized or raw device strings as relevant from strategy.
|
|
def __init__(self,
|
|
worker_device_pairs,
|
|
canonicalize_devices=True):
|
|
"""Initialize an `InputWorkers` object.
|
|
|
|
Args:
|
|
worker_device_pairs: A sequence of pairs: `(input device, a tuple of
|
|
compute devices fed by that input device)`.
|
|
canonicalize_devices: Whether to canonicalize devices for workers fully or
|
|
partially. If False, it will partially canonicalize devices by removing
|
|
job and task.
|
|
"""
|
|
self._worker_device_pairs = worker_device_pairs
|
|
self._input_worker_devices = tuple(d for d, _ in self._worker_device_pairs)
|
|
self._canonicalize_devices = canonicalize_devices
|
|
if canonicalize_devices:
|
|
self._fed_devices = tuple(
|
|
tuple(device_util.canonicalize(d)
|
|
for d in f)
|
|
for _, f in self._worker_device_pairs)
|
|
else:
|
|
self._fed_devices = tuple(
|
|
tuple(device_util.canonicalize_without_job_and_task(d)
|
|
for d in f)
|
|
for _, f in self._worker_device_pairs)
|
|
|
|
@property
|
|
def num_workers(self):
|
|
return len(self._input_worker_devices)
|
|
|
|
@property
|
|
def worker_devices(self):
|
|
return self._input_worker_devices
|
|
|
|
def compute_devices_for_worker(self, worker_index):
|
|
return self._fed_devices[worker_index]
|
|
|
|
def __repr__(self):
|
|
devices = self.worker_devices
|
|
debug_repr = ",\n".join(" %d %s: %s" %
|
|
(i, devices[i], self._fed_devices[i])
|
|
for i in range(len(devices)))
|
|
return "%s:{\n%s}" % (self.__class__.__name__, debug_repr)
|
|
|
|
def serialize(self):
|
|
return (self._worker_device_pairs, self._canonicalize_devices)
|
|
|
|
def deserialize(self, serialized):
|
|
return InputWorkers(serialized)
|
|
|
|
|
|
def _calculate_replicas_with_values(strategy, input_workers, optional_list):
|
|
"""Computes the number of replicas that have values.
|
|
|
|
Args:
|
|
strategy: the `tf.distribute.Strategy`.
|
|
input_workers: the `InputWorkers`.
|
|
optional_list: a list of lists `tf.experimental.Optional`. The values from
|
|
each compute device grouped by the input device.
|
|
|
|
Returns:
|
|
A scalar Tensor.
|
|
"""
|
|
worker_has_values = []
|
|
for worker, optionals in zip(input_workers.worker_devices, optional_list):
|
|
with ops.device(worker):
|
|
device_has_values = [
|
|
math_ops.cast(v.has_value(), dtypes.int64) for v in optionals
|
|
]
|
|
worker_has_values.append(
|
|
math_ops.reduce_sum(device_has_values, keepdims=True))
|
|
client_has_values = math_ops.reduce_sum(worker_has_values, keepdims=True)
|
|
if strategy.extended._in_multi_worker_mode(): # pylint: disable=protected-access
|
|
global_has_values = strategy.reduce(
|
|
reduce_util.ReduceOp.SUM, client_has_values, axis=None)
|
|
return array_ops.reshape(global_has_values, [])
|
|
else:
|
|
return array_ops.reshape(client_has_values, [])
|
|
|
|
|
|
def _is_statically_shaped(element_spec):
|
|
"""Test if an iterator output is statically shaped.
|
|
|
|
For sparse and ragged tensors this only tests the batch dimension.
|
|
|
|
Args:
|
|
element_spec: a nest structure of `tf.TypeSpec`. The element spec of the
|
|
dataset of the iterator.
|
|
|
|
Returns:
|
|
True if the shape is static, false otherwise.
|
|
"""
|
|
|
|
for spec in nest.flatten(element_spec):
|
|
if isinstance(
|
|
spec, (sparse_tensor.SparseTensorSpec, ragged_tensor.RaggedTensorSpec)):
|
|
# For sparse or ragged tensor, we should only check the first
|
|
# dimension in order to get_next_as_optional. This is because
|
|
# when these tensors get batched by dataset only the batch dimension
|
|
# is set.
|
|
if spec.shape.rank > 0 and spec.shape.as_list()[0] is None:
|
|
return False
|
|
else:
|
|
for component in spec._flat_tensor_specs: # pylint: disable=protected-access
|
|
if not component.shape.is_fully_defined():
|
|
return False
|
|
return True
|
|
|
|
|
|
class DistributedIteratorBase(collections_abc.Iterator,
|
|
distribute_types.DistributedIteratorInterface):
|
|
"""Common implementation for all input iterators."""
|
|
|
|
# pylint: disable=super-init-not-called
|
|
def __init__(
|
|
self,
|
|
input_workers,
|
|
iterators,
|
|
strategy,
|
|
cardinality,
|
|
enable_get_next_as_optional,
|
|
replica_order=None,
|
|
):
|
|
assert isinstance(input_workers, InputWorkers)
|
|
if not input_workers.worker_devices:
|
|
raise ValueError("Should have at least one worker for input iterator.")
|
|
|
|
self._iterators = iterators
|
|
self._input_workers = input_workers
|
|
self._strategy = strategy
|
|
self._cardinality = cardinality
|
|
self._enable_get_next_as_optional = enable_get_next_as_optional
|
|
self._replica_order = replica_order
|
|
|
|
def next(self):
|
|
return self.__next__()
|
|
|
|
def __next__(self):
|
|
try:
|
|
return self.get_next()
|
|
except errors.OutOfRangeError:
|
|
raise StopIteration
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def get_next_as_optional(self):
|
|
# Ideally get_next_as_optional() should be consistent with get_next(), but
|
|
# we used to always do partial batch handling in get_next_as_optional(). We
|
|
# are keeping this behavior for now until we understantd the impact.
|
|
|
|
# Skip partial batch handling when the dataset is infinite or empty, as
|
|
# there won't be any partial batches in those cases. This gives the user
|
|
# more static shapes as it avoids the tf.cond. Note that for empty datasets,
|
|
# we can only skip in single client mode, as the dataset can be non-empty on
|
|
# other workers.
|
|
if self._cardinality == cardinality_lib.INFINITE:
|
|
return optional_ops.Optional.from_value(
|
|
self._get_next_no_partial_batch_handling())
|
|
if (self._cardinality == 0 and
|
|
not self._strategy.extended._in_multi_worker_mode()): # pylint: disable=protected-access
|
|
return optional_ops.Optional.empty(self._element_spec)
|
|
|
|
optional_list = []
|
|
for i, worker in enumerate(self._input_workers.worker_devices):
|
|
with ops.device(worker):
|
|
optional_list.append(self._iterators[i].get_next_as_optional_list())
|
|
|
|
def _create_optional_with_dummy():
|
|
value_list = _get_value_or_dummy(
|
|
self._input_workers, optional_list, produce_dummy=True)
|
|
|
|
if self._replica_order is not None:
|
|
value_list = self._reorder_replicas(value_list)
|
|
|
|
per_replica = _create_per_replica(value_list, self._strategy)
|
|
return optional_ops.Optional.from_value(per_replica)
|
|
|
|
def _create_empty_optional():
|
|
return optional_ops.Optional.empty(self._element_spec)
|
|
|
|
num_replicas_with_values = _calculate_replicas_with_values(
|
|
self._strategy, self._input_workers, optional_list)
|
|
|
|
return tf_cond.cond(
|
|
num_replicas_with_values > 0,
|
|
_create_optional_with_dummy,
|
|
_create_empty_optional,
|
|
strict=True)
|
|
|
|
def get_next(self, name=None):
|
|
"""Returns the next input from the iterator for all replicas."""
|
|
with distribute_lib.enter_or_assert_strategy(
|
|
self._strategy):
|
|
if distribute_lib.get_replica_context() is not None:
|
|
raise ValueError("next(iterator) should be called from outside of "
|
|
"replica_fn. e.g. strategy.run(replica_fn, "
|
|
"args=(next(iterator),))")
|
|
|
|
if not self._enable_get_next_as_optional:
|
|
return self._get_next_no_partial_batch_handling(name)
|
|
|
|
optional_list = []
|
|
for i, worker in enumerate(self._input_workers.worker_devices):
|
|
with ops.device(worker):
|
|
optional_list.append(self._iterators[i].get_next_as_optional_list())
|
|
num_replicas_with_values = _calculate_replicas_with_values(
|
|
self._strategy, self._input_workers, optional_list)
|
|
|
|
def _value_or_dummy():
|
|
value_list = _get_value_or_dummy(
|
|
self._input_workers, optional_list, produce_dummy=True)
|
|
|
|
if self._replica_order is not None:
|
|
value_list = self._reorder_replicas(value_list)
|
|
|
|
return _create_per_replica(value_list, self._strategy)
|
|
|
|
def _eof():
|
|
# Optional.get_value raises InvalidArgumentError when there's no value,
|
|
# so we need to call GetNext to raise EOFError.
|
|
return self._get_next_no_partial_batch_handling()
|
|
|
|
return tf_cond.cond(
|
|
num_replicas_with_values > 0, _value_or_dummy, _eof, strict=True)
|
|
|
|
def _get_next_no_partial_batch_handling(self, name=None):
|
|
replicas = []
|
|
for i, worker in enumerate(self._input_workers.worker_devices):
|
|
if name is not None:
|
|
d = tf_device.DeviceSpec.from_string(worker)
|
|
new_name = "%s_%s_%d" % (name, d.job, d.task)
|
|
else:
|
|
new_name = None
|
|
with ops.device(worker):
|
|
# Make `replicas` a flat list of values across all replicas.
|
|
replicas.extend(self._iterators[i].get_next_as_list(new_name))
|
|
|
|
if self._replica_order is not None:
|
|
replicas = self._reorder_replicas(replicas)
|
|
|
|
return _create_per_replica(replicas, self._strategy)
|
|
|
|
def _reorder_replicas(self, replicas):
|
|
assert len(self._replica_order) == len(
|
|
replicas
|
|
), "replica order size ({}) != replicas size ({})!".format(
|
|
len(self._replica_order), len(replicas)
|
|
)
|
|
return [replicas[i] for i in self._replica_order]
|
|
|
|
|
|
class DistributedDatasetAndIteratorSpec(type_spec.TypeSpec):
|
|
"""Common Type specification for `DistributedDataset and DistributedDatasetsFromFunction."""
|
|
|
|
__slots__ = [
|
|
"_input_workers", "_element_spec", "_strategy", "_cardinality",
|
|
"_enable_get_next_as_optional", "_options", "_canonicalize_devices"
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
input_workers,
|
|
element_spec,
|
|
strategy,
|
|
options,
|
|
cardinality=cardinality_lib.UNKNOWN,
|
|
enable_get_next_as_optional=None,
|
|
replica_order=None,
|
|
):
|
|
# We don't want to allow deserialization of this class because we don't
|
|
# serialize the strategy object. Currently the only places where
|
|
# _deserialize is called is when we save/restore using SavedModels.
|
|
if isinstance(input_workers, tuple):
|
|
raise NotImplementedError("DistributedIteratorSpec does not have support "
|
|
"for deserialization.")
|
|
else:
|
|
self._input_workers = input_workers
|
|
self._element_spec = element_spec
|
|
self._strategy = strategy
|
|
self._cardinality = cardinality
|
|
self._enable_get_next_as_optional = enable_get_next_as_optional
|
|
self._options = options
|
|
if self._strategy:
|
|
self._canonicalize_devices = getattr(self._strategy,
|
|
"_canonicalize_devices", True)
|
|
else:
|
|
self._canonicalize_devices = True
|
|
self._replica_order = replica_order
|
|
|
|
def _serialize(self):
|
|
# We cannot serialize the strategy object so we convert it to an id that we
|
|
# can use for comparison.
|
|
return (self._input_workers.serialize(), self._element_spec,
|
|
id(self._strategy), id(self._options))
|
|
|
|
def _deserialize(self):
|
|
raise ValueError(
|
|
f"Deserialization is currently unsupported for {type(self)}.")
|
|
|
|
def sanity_check_type(self, other):
|
|
"""Returns the most specific TypeSpec compatible with `self` and `other`.
|
|
|
|
Args:
|
|
other: A `TypeSpec`.
|
|
|
|
Raises:
|
|
ValueError: If there is no TypeSpec that is compatible with both `self`
|
|
and `other`.
|
|
"""
|
|
# pylint: disable=protected-access
|
|
if type(self) is not type(other):
|
|
raise ValueError("No TypeSpec is compatible with both %s and %s" %
|
|
(self, other))
|
|
if self._input_workers.serialize() != other._input_workers.serialize():
|
|
raise ValueError("_input_workers is not compatible with both %s "
|
|
"and %s" % (self, other))
|
|
if self._strategy is not other._strategy:
|
|
raise ValueError("tf.distribute strategy is not compatible with both %s "
|
|
"and %s" % (self, other))
|
|
|
|
def is_subtype_of(self, other):
|
|
"""Returns True if `self` is subtype of `other`.
|
|
|
|
Args:
|
|
other: A `TypeSpec`.
|
|
"""
|
|
try:
|
|
self.sanity_check_type(other)
|
|
nest.assert_same_structure(self._element_spec, other._element_spec) # pylint: disable=protected-access
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
self_elements = nest.flatten(self._element_spec)
|
|
other_elements = nest.flatten(other._element_spec) # pylint: disable=protected-access
|
|
|
|
return all(
|
|
self_element.is_subtype_of(other_element)
|
|
for (self_element, other_element) in zip(self_elements, other_elements))
|
|
|
|
def most_specific_common_supertype(self, others):
|
|
"""Returns the most specific supertype of `self` and `others`.
|
|
|
|
Args:
|
|
others: A Sequence of `TypeSpec`.
|
|
|
|
Returns `None` if a supertype does not exist.
|
|
"""
|
|
try:
|
|
for other in others:
|
|
self.sanity_check_type(other)
|
|
nest.assert_same_structure(self._element_spec, other._element_spec) # pylint: disable=protected-access
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
self_elements = nest.flatten(self._element_spec)
|
|
others_elements = [nest.flatten(other._element_spec) for other in others] # pylint: disable=protected-access
|
|
common_elements = [None] * len(self_elements)
|
|
|
|
for i, self_element in enumerate(self_elements):
|
|
common_elements[i] = self_element.most_specific_common_supertype(
|
|
[other_elements[i] for other_elements in others_elements])
|
|
if common_elements[i] is None:
|
|
return None
|
|
common_element_spec = nest.pack_sequence_as(self._element_spec,
|
|
common_elements)
|
|
return type(self)(
|
|
self._input_workers,
|
|
common_element_spec,
|
|
self._strategy,
|
|
self._options,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional)
|
|
|
|
def _with_tensor_ranks_only(self):
|
|
element_spec = nest.map_structure(
|
|
lambda s: s._with_tensor_ranks_only(), # pylint: disable=protected-access
|
|
self._element_spec)
|
|
return type(self)(
|
|
self._input_workers,
|
|
element_spec,
|
|
self._strategy,
|
|
self._options,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional)
|
|
|
|
# TODO(b/206014848): Remove once names are not used.
|
|
def _without_tensor_names(self):
|
|
element_spec = nest.map_structure(
|
|
lambda s: s._without_tensor_names(), # pylint: disable=protected-access
|
|
self._element_spec)
|
|
return type(self)(
|
|
self._input_workers,
|
|
element_spec,
|
|
self._strategy,
|
|
self._options,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional)
|
|
|
|
|
|
class DistributedIteratorSpec(DistributedDatasetAndIteratorSpec):
|
|
"""Type specification for `DistributedIterator`."""
|
|
|
|
@property
|
|
def value_type(self):
|
|
return DistributedIterator
|
|
|
|
@property
|
|
def _component_specs(self):
|
|
specs = []
|
|
worker_device_pairs = self._input_workers._worker_device_pairs # pylint: disable=protected-access
|
|
|
|
for i, (input_device, compute_devices) in enumerate(worker_device_pairs):
|
|
element_spec = nest.map_structure(
|
|
functools.partial(_replace_per_replica_spec, i=i), self._element_spec)
|
|
specs.append(
|
|
_SingleWorkerDatasetIteratorSpec(input_device, compute_devices,
|
|
element_spec, self._options,
|
|
self._canonicalize_devices))
|
|
return specs
|
|
|
|
def _to_components(self, value):
|
|
return value._iterators # pylint: disable=protected-access
|
|
|
|
def _from_components(self, components):
|
|
return DistributedIterator(
|
|
input_workers=self._input_workers,
|
|
iterators=None,
|
|
components=components,
|
|
element_spec=self._element_spec,
|
|
strategy=self._strategy,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional,
|
|
options=self._options,
|
|
replica_order=self._replica_order,
|
|
)
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
# pylint: disable=protected-access
|
|
return DistributedIteratorSpec(
|
|
value._input_workers,
|
|
value._element_spec,
|
|
value._strategy,
|
|
value._options,
|
|
cardinality=value._cardinality,
|
|
enable_get_next_as_optional=value._enable_get_next_as_optional)
|
|
|
|
|
|
class DistributedIterator(DistributedIteratorBase,
|
|
composite_tensor.CompositeTensor):
|
|
"""Input Iterator for a distributed dataset."""
|
|
|
|
def __init__(
|
|
self,
|
|
input_workers=None,
|
|
iterators=None,
|
|
strategy=None,
|
|
components=None,
|
|
element_spec=None,
|
|
cardinality=cardinality_lib.UNKNOWN,
|
|
enable_get_next_as_optional=False,
|
|
options=None,
|
|
replica_order=None,
|
|
):
|
|
if input_workers is None:
|
|
raise ValueError("`input_workers` should be "
|
|
"provided.")
|
|
|
|
error_message = ("Either `input_workers` or "
|
|
"both `components` and `element_spec` need to be "
|
|
"provided.")
|
|
self._options = options
|
|
|
|
if iterators is None:
|
|
if (components is None or element_spec is None):
|
|
raise ValueError(error_message)
|
|
self._element_spec = element_spec
|
|
self._input_workers = input_workers
|
|
self._iterators = components
|
|
self._strategy = strategy
|
|
self._cardinality = cardinality
|
|
self._enable_get_next_as_optional = enable_get_next_as_optional
|
|
self._replica_order = replica_order
|
|
else:
|
|
if (components is not None and element_spec is not None):
|
|
raise ValueError(error_message)
|
|
|
|
super(DistributedIterator, self).__init__(
|
|
input_workers,
|
|
iterators,
|
|
strategy,
|
|
cardinality,
|
|
enable_get_next_as_optional,
|
|
replica_order,
|
|
)
|
|
|
|
@property
|
|
def element_spec(self):
|
|
# When partial batch handling is enabled, always set the batch dimension to
|
|
# None, otherwise we just follow element_spec of the underlying dataset
|
|
# (whose batch dimension may also be None). This is because with partial
|
|
# batching handling we could always produce empty batches.
|
|
if (self._enable_get_next_as_optional and
|
|
self._strategy.extended._in_multi_worker_mode()): # pylint: disable=protected-access
|
|
return nest.map_structure(
|
|
_rebatch_as_dynamic, self._element_spec, expand_composites=False)
|
|
return self._element_spec
|
|
|
|
@property
|
|
def _type_spec(self):
|
|
# Note that we use actual element_spec instead of the rebatched-as-dynamic
|
|
# one to create DistributedIteratorSpec, to be consistent with the
|
|
# underlying iterators' specs.
|
|
return DistributedIteratorSpec(
|
|
self._input_workers,
|
|
self._element_spec,
|
|
self._strategy,
|
|
self._options,
|
|
self._cardinality,
|
|
self._enable_get_next_as_optional,
|
|
self._replica_order,
|
|
)
|
|
|
|
|
|
class _IterableInput(collections_abc.Iterable,
|
|
distribute_types.DistributedDatasetInterface):
|
|
"""Base class for iterable inputs for distribution strategies."""
|
|
|
|
# pylint: disable=super-init-not-called
|
|
def __init__(self, input_workers):
|
|
assert isinstance(input_workers, InputWorkers)
|
|
self._input_workers = input_workers
|
|
|
|
def __iter__(self):
|
|
raise NotImplementedError("must be implemented in descendants")
|
|
|
|
def reduce(self, initial_state, reduce_fn):
|
|
"""Execute a `reduce_fn` over all the elements of the input."""
|
|
iterator = iter(self)
|
|
optional_data = iterator.get_next_as_optional()
|
|
|
|
def cond(optional_data, state):
|
|
del state # Unused.
|
|
return optional_data.has_value()
|
|
|
|
def loop_body(optional_data, state):
|
|
"""Executes `reduce_fn` in a loop till the dataset is empty."""
|
|
state = reduce_fn(state, optional_data.get_value())
|
|
optional_data = iterator.get_next_as_optional()
|
|
return optional_data, state
|
|
|
|
optional_data, final_state = while_loop.while_loop(
|
|
cond,
|
|
loop_body, [optional_data, initial_state],
|
|
parallel_iterations=1,
|
|
return_same_structure=True)
|
|
return final_state
|
|
|
|
|
|
class DistributedDatasetSpec(DistributedDatasetAndIteratorSpec):
|
|
"""Type specification for `DistributedDataset."""
|
|
|
|
@property
|
|
def value_type(self):
|
|
return DistributedDataset
|
|
|
|
@property
|
|
def _component_specs(self):
|
|
specs = []
|
|
worker_device_pairs = self._input_workers._worker_device_pairs # pylint: disable=protected-access
|
|
|
|
for i, _ in enumerate(worker_device_pairs):
|
|
element_spec = nest.map_structure(
|
|
functools.partial(_replace_per_replica_spec, i=i), self._element_spec)
|
|
specs.append(dataset_ops.DatasetSpec(element_spec))
|
|
return specs
|
|
|
|
def _to_components(self, value):
|
|
return value._cloned_datasets # pylint: disable=protected-access
|
|
|
|
def _from_components(self, components):
|
|
return DistributedDataset(
|
|
input_workers=self._input_workers,
|
|
strategy=self._strategy,
|
|
components=components,
|
|
element_spec=self._element_spec,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional,
|
|
options=self._options,
|
|
replica_order=self._replica_order,
|
|
)
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
# pylint: disable=protected-access
|
|
return DistributedDatasetSpec(
|
|
value._input_workers,
|
|
value._element_spec,
|
|
value._strategy,
|
|
value._options,
|
|
enable_get_next_as_optional=value._enable_get_next_as_optional)
|
|
# pylint: enable=protected-access
|
|
|
|
|
|
class DistributedDataset(_IterableInput, composite_tensor.CompositeTensor):
|
|
"""Distributed dataset that supports prefetching to multiple devices."""
|
|
|
|
def __init__(
|
|
self,
|
|
input_workers,
|
|
strategy,
|
|
dataset=None,
|
|
num_replicas_in_sync=None,
|
|
input_context=None,
|
|
components=None,
|
|
element_spec=None,
|
|
enable_get_next_as_optional=None,
|
|
build=True,
|
|
options=None,
|
|
replica_order=None,
|
|
):
|
|
"""Distribute the dataset on all workers.
|
|
|
|
If `num_replicas_in_sync` is not None, we split each batch of the dataset
|
|
into `num_replicas_in_sync` smaller batches, to be distributed among that
|
|
worker's replicas, so that the batch size for a global step (across all
|
|
workers and replicas) is as expected.
|
|
|
|
Args:
|
|
input_workers: an `InputWorkers` object.
|
|
strategy: a `tf.distribute.Strategy` object, used to run all-reduce to
|
|
handle last partial batch.
|
|
dataset: `tf.data.Dataset` that will be used as the input source. Either
|
|
dataset or components field should be passed when constructing
|
|
DistributedDataset. Use this when constructing DistributedDataset from a
|
|
new `tf.data.Dataset`. Use components when constructing using
|
|
DistributedDatasetSpec.
|
|
num_replicas_in_sync: Optional integer. If this is not None, the value is
|
|
used to decide how to rebatch datasets into smaller batches so that the
|
|
total batch size for each step (across all workers and replicas) adds up
|
|
to `dataset`'s batch size.
|
|
input_context: `InputContext` for sharding. Only pass this in for between
|
|
graph multi-worker cases where there is only one `input_worker`. In
|
|
these cases, we will shard based on the `input_pipeline_id` and
|
|
`num_input_pipelines` in the `InputContext`.
|
|
components: datasets when DistributedDataset is constructed from
|
|
DistributedDatasetSpec. Either field dataset or components should be
|
|
passed.
|
|
element_spec: element spec for DistributedDataset when constructing from
|
|
DistributedDatasetSpec. This will be used to set the element_spec for
|
|
DistributedDataset and verified against element_spec from components.
|
|
enable_get_next_as_optional: this is required when components is passed
|
|
instead of dataset.
|
|
build: whether to build underlying datasets when this object is created.
|
|
This is only useful for `ParameterServerStrategy` now.
|
|
options: `tf.distribute.InputOptions` used to control options on how this
|
|
dataset is distributed.
|
|
replica_order: the order of the replicas, which will be used to reorder
|
|
the iterators to match the device order.
|
|
"""
|
|
super(DistributedDataset, self).__init__(input_workers=input_workers)
|
|
if input_workers is None or strategy is None:
|
|
raise ValueError("input_workers and strategy are required arguments")
|
|
if dataset is not None and components is not None:
|
|
raise ValueError("Only one of dataset or components should be present")
|
|
if dataset is None and components is None:
|
|
raise ValueError("At least one of dataset or components should be passed")
|
|
|
|
self._input_workers = input_workers
|
|
self._strategy = strategy
|
|
self._options = options
|
|
self._input_context = input_context
|
|
self._num_replicas_in_sync = num_replicas_in_sync
|
|
self._replica_order = replica_order
|
|
|
|
if dataset is not None:
|
|
self._original_dataset = dataset
|
|
self._built = False
|
|
if build:
|
|
self.build()
|
|
else:
|
|
if not build:
|
|
raise ValueError(
|
|
"When constructing DistributedDataset with components, build "
|
|
"should not be False. This is an internal error. Please file a "
|
|
"bug.")
|
|
if enable_get_next_as_optional is None:
|
|
raise ValueError(
|
|
"When constructing DistributedDataset with components, " +
|
|
"enable_get_next_as_optional should also be passed")
|
|
self._cloned_datasets = components
|
|
self._cardinality = _cardinality(self._cloned_datasets[0])
|
|
self._enable_get_next_as_optional = enable_get_next_as_optional
|
|
|
|
assert element_spec is not None
|
|
if element_spec != _create_distributed_tensor_spec(
|
|
self._strategy, self._cloned_datasets[0].element_spec):
|
|
raise ValueError("Mismatched element_spec from the passed components")
|
|
self._element_spec = element_spec
|
|
|
|
self._built = True
|
|
|
|
def build(self, dataset_to_replace=None):
|
|
assert not self._built
|
|
dataset = dataset_to_replace or self._original_dataset
|
|
self._cardinality = _cardinality(dataset)
|
|
self._enable_get_next_as_optional = _enable_get_next_as_optional(
|
|
self._strategy, dataset, self._cardinality)
|
|
distribute_start_time_ns = time.time_ns()
|
|
self._create_cloned_datasets_from_dataset(dataset, self._input_context,
|
|
self._input_workers,
|
|
self._strategy,
|
|
self._num_replicas_in_sync)
|
|
if context.executing_eagerly():
|
|
# Records the time to initialize the distributed dataset.
|
|
context.async_wait()
|
|
distribute_duration_ms = (time.time_ns() -
|
|
distribute_start_time_ns) // 1_000_000
|
|
_distributed_dataset_initialization_time_milliseconds.get_cell(
|
|
self._strategy.__class__.__name__,
|
|
str(self._input_workers.num_workers)).add(distribute_duration_ms)
|
|
self._element_spec = _create_distributed_tensor_spec(
|
|
self._strategy, self._cloned_datasets[0].element_spec)
|
|
self._built = True
|
|
|
|
def auto_shard(self, num_shards, shard_ix):
|
|
assert (
|
|
len(self._cloned_datasets) == len(self._input_workers.worker_devices)
|
|
), (
|
|
f"datasets: {len(self._cloned_datasets)}, "
|
|
f"input workers: {len(self._input_workers.worker_devices)}"
|
|
)
|
|
sharded_datasets = []
|
|
for i in range(len(self._input_workers.worker_devices)):
|
|
with ops.colocate_with(self._cloned_datasets[i]._variant_tensor): # pylint:disable=protected-access
|
|
sharded_datasets.append(
|
|
input_ops.auto_shard_dataset(
|
|
self._cloned_datasets[i], num_shards, shard_ix,
|
|
self._num_replicas_in_sync
|
|
))
|
|
return DistributedDataset(
|
|
self._input_workers,
|
|
self._strategy,
|
|
components=sharded_datasets,
|
|
element_spec=self._element_spec,
|
|
options=self._options,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional)
|
|
|
|
@property
|
|
def cardinality(self):
|
|
if not self._built:
|
|
raise ValueError(
|
|
"Cannot get the cardinality of a dataset that is not built")
|
|
return self._cardinality
|
|
|
|
def _create_cloned_datasets_from_dataset(self, dataset, input_context,
|
|
input_workers, strategy,
|
|
num_replicas_in_sync):
|
|
# We clone and shard the dataset on each worker. The current setup tries to
|
|
# shard the dataset by files if possible so that each worker sees a
|
|
# different subset of files. If that is not possible, will attempt to shard
|
|
# the final input such that each worker will run the entire preprocessing
|
|
# pipeline and only receive its own shard of the dataset.
|
|
|
|
# Additionally, we rebatch the dataset on each worker into
|
|
# `num_replicas_in_sync` smaller batches to be distributed among that
|
|
# worker's replicas, so that the batch size for a global step (across all
|
|
# workers and replicas) adds up to the original dataset's batch size.
|
|
if num_replicas_in_sync is not None and num_replicas_in_sync > 1:
|
|
num_workers = input_context.num_input_pipelines if input_context else len(
|
|
input_workers.worker_devices)
|
|
rebatch_fn = self._make_rebatch_fn(dataset, num_workers,
|
|
num_replicas_in_sync)
|
|
else:
|
|
rebatch_fn = None
|
|
self._cloned_datasets = []
|
|
if input_context:
|
|
# Between-graph where we rely on the input_context for sharding
|
|
assert input_workers.num_workers == 1
|
|
if rebatch_fn is not None:
|
|
dataset = rebatch_fn(dataset, input_context.input_pipeline_id)
|
|
dataset = input_ops.auto_shard_dataset(dataset,
|
|
input_context.num_input_pipelines,
|
|
input_context.input_pipeline_id,
|
|
num_replicas_in_sync)
|
|
self._cloned_datasets.append(dataset)
|
|
else:
|
|
replicated_ds = distribute.replicate(dataset,
|
|
input_workers.worker_devices)
|
|
for i, worker in enumerate(input_workers.worker_devices):
|
|
with ops.device(worker):
|
|
cloned_dataset = replicated_ds[worker]
|
|
if rebatch_fn is not None:
|
|
cloned_dataset = rebatch_fn(cloned_dataset, i)
|
|
cloned_dataset = input_ops.auto_shard_dataset(
|
|
cloned_dataset, len(input_workers.worker_devices), i,
|
|
num_replicas_in_sync)
|
|
self._cloned_datasets.append(cloned_dataset)
|
|
|
|
def _make_rebatch_fn(self, dataset, num_workers, num_replicas_in_sync):
|
|
"""Returns a callable that rebatches the input dataset.
|
|
|
|
Args:
|
|
dataset: A `tf.data.Dataset` representing the dataset to be distributed.
|
|
num_workers: An integer representing the number of workers to distribute
|
|
`dataset` among.
|
|
num_replicas_in_sync: An integer representing the number of replicas in
|
|
sync across all workers.
|
|
"""
|
|
if num_replicas_in_sync % num_workers:
|
|
raise ValueError(
|
|
"tf.distribute expects every worker to have the same number of "
|
|
"replicas. However, encountered `num_replicas_in_sync` ({}) that "
|
|
"cannot be divided by `num_workers` ({})".format(
|
|
num_replicas_in_sync, num_workers))
|
|
|
|
num_replicas_per_worker = num_replicas_in_sync // num_workers
|
|
with ops.colocate_with(dataset._variant_tensor): # pylint: disable=protected-access
|
|
batch_size = distribute.compute_batch_size(dataset)
|
|
|
|
def rebatch_fn(dataset, worker_index):
|
|
try:
|
|
|
|
def apply_rebatch():
|
|
batch_sizes = distribute.batch_sizes_for_worker(
|
|
batch_size, num_workers, num_replicas_per_worker, worker_index)
|
|
return dataset.rebatch(batch_sizes).prefetch(num_replicas_per_worker)
|
|
|
|
# pylint: disable=protected-access
|
|
def apply_legacy_rebatch():
|
|
return distribute._LegacyRebatchDataset(
|
|
dataset, num_replicas_in_sync).prefetch(num_replicas_per_worker)
|
|
|
|
with ops.colocate_with(dataset._variant_tensor):
|
|
return tf_cond.cond(
|
|
math_ops.not_equal(batch_size, -1),
|
|
true_fn=apply_rebatch,
|
|
false_fn=apply_legacy_rebatch)
|
|
except errors.InvalidArgumentError as e:
|
|
if "without encountering a batch" in str(e):
|
|
six.reraise(
|
|
ValueError,
|
|
ValueError(
|
|
"Call the `batch` method on the input Dataset in order to be "
|
|
"able to split your input across {} replicas.\n Please see "
|
|
"the tf.distribute.Strategy guide. {}".format(
|
|
num_replicas_in_sync, e)),
|
|
sys.exc_info()[2])
|
|
else:
|
|
raise
|
|
|
|
return rebatch_fn
|
|
|
|
def __iter__(self):
|
|
if not (context.executing_eagerly() or
|
|
ops.get_default_graph().building_function):
|
|
raise RuntimeError("__iter__() is only supported inside of tf.function "
|
|
"or when eager execution is enabled.")
|
|
if not self._built:
|
|
raise ValueError("To use this dataset, you need to pass this dataset to "
|
|
"ClusterCoordinator.create_per_worker_dataset.")
|
|
|
|
canonicalize_devices = getattr(self._strategy, "_canonicalize_devices",
|
|
True)
|
|
|
|
worker_iterators = _create_iterators_per_worker(
|
|
self._cloned_datasets,
|
|
self._input_workers,
|
|
options=self._options,
|
|
canonicalize_devices=canonicalize_devices)
|
|
iterator = DistributedIterator(
|
|
self._input_workers,
|
|
worker_iterators,
|
|
self._strategy,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional,
|
|
options=self._options,
|
|
replica_order=self._replica_order,
|
|
)
|
|
iterator._element_spec = self._element_spec # pylint: disable=protected-access
|
|
|
|
# When async eager is enabled, sometimes the iterator may not finish
|
|
# initialization before passing to a multi device function, add a sync point
|
|
# here to make sure all underlying iterators are initialized.
|
|
if context.executing_eagerly():
|
|
context.async_wait()
|
|
|
|
return iterator
|
|
|
|
@property
|
|
def element_spec(self):
|
|
"""The type specification of an element of this dataset."""
|
|
# When partial batch handling is enabled, always set the batch dimension to
|
|
# None, otherwise we just follow element_spec of the underlying dataset
|
|
# (whose batch dimension may also be None). This is because with partial
|
|
# batching handling we could always produce empty batches.
|
|
if (self._enable_get_next_as_optional and
|
|
self._strategy.extended._in_multi_worker_mode()): # pylint: disable=protected-access
|
|
return nest.map_structure(
|
|
_rebatch_as_dynamic, self._element_spec, expand_composites=False)
|
|
return self._element_spec
|
|
|
|
@property
|
|
def _type_spec(self):
|
|
return DistributedDatasetSpec(
|
|
self._input_workers,
|
|
self._element_spec,
|
|
self._strategy,
|
|
self._options,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional)
|
|
|
|
|
|
class DistributedDatasetsFromFunctionSpec(DistributedDatasetAndIteratorSpec):
|
|
"""Type specification for `DistributedDatasetsFromFunction."""
|
|
|
|
@property
|
|
def value_type(self):
|
|
return DistributedDatasetsFromFunction
|
|
|
|
@property
|
|
def _component_specs(self):
|
|
specs = []
|
|
worker_device_pairs = self._input_workers._worker_device_pairs # pylint: disable=protected-access
|
|
|
|
for i, _ in enumerate(worker_device_pairs):
|
|
element_spec = nest.map_structure(
|
|
functools.partial(_replace_per_replica_spec, i=i), self._element_spec)
|
|
specs.append(dataset_ops.DatasetSpec(element_spec))
|
|
return specs
|
|
|
|
def _to_components(self, value):
|
|
return value._datasets # pylint: disable=protected-access
|
|
|
|
def _from_components(self, components):
|
|
return DistributedDatasetsFromFunction(
|
|
input_workers=self._input_workers,
|
|
strategy=self._strategy,
|
|
components=components,
|
|
element_spec=self._element_spec,
|
|
options=self._options)
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
# pylint: disable=protected-access
|
|
return DistributedDatasetsFromFunctionSpec(
|
|
input_workers=value._input_workers,
|
|
element_spec=value._element_spec,
|
|
strategy=value._strategy,
|
|
options=value._options)
|
|
|
|
|
|
# TODO(priyag): Add other replication modes.
|
|
class DistributedDatasetsFromFunction(_IterableInput,
|
|
composite_tensor.CompositeTensor):
|
|
"""Inputs created from dataset function."""
|
|
|
|
def __init__(
|
|
self,
|
|
input_workers,
|
|
strategy,
|
|
input_contexts=None,
|
|
dataset_fn=None,
|
|
options=None,
|
|
components=None,
|
|
element_spec=None,
|
|
build=True,
|
|
replica_order=None,
|
|
):
|
|
"""Makes an iterable from datasets created by the given function.
|
|
|
|
Args:
|
|
input_workers: an `InputWorkers` object.
|
|
strategy: a `tf.distribute.Strategy` object, used to run all-reduce to
|
|
handle last partial batch.
|
|
input_contexts: A list of `InputContext` instances to be passed to call(s)
|
|
to `dataset_fn`. Length and order should match worker order in
|
|
`worker_device_pairs`.
|
|
dataset_fn: A function that returns a `Dataset` given an `InputContext`.
|
|
Either dataset_fn or components should be passed to construct
|
|
DistributedDatasetsFromFunction. Use this when constructing
|
|
DistributedDataset using a function. Use components when constructing
|
|
using DistributedDatasetsFromFunctionSpec.
|
|
options: `tf.distribute.InputOptions` used to control options on how this
|
|
dataset is distributed.
|
|
components: datasets when DistributedDatasetsFromFunction is constructed
|
|
from DistributedDatasetsFromFunctionSpec. Only one of dataset or
|
|
components should be passed.
|
|
element_spec: element spec for DistributedDataset when constructing from
|
|
DistributedDatasetSpec. This will be used to set the element_spec for
|
|
DistributedDatasetsFromFunctionSpec and verified against element_spec
|
|
from components.
|
|
build: whether to build underlying datasets when this object is created.
|
|
This is only useful for `ParameterServerStrategy` now.
|
|
replica_order: the order of the replicas, which will be used to reorder
|
|
the iterators to match the device order.
|
|
"""
|
|
super(DistributedDatasetsFromFunction, self).__init__(
|
|
input_workers=input_workers)
|
|
self._input_workers = input_workers
|
|
self._strategy = strategy
|
|
self._options = options
|
|
self._replica_order = replica_order
|
|
if dataset_fn is not None and components is not None:
|
|
raise ValueError("Only one of dataset_fn or components should be set")
|
|
if dataset_fn is None and components is None:
|
|
raise ValueError("At least one of dataset_fn or components should be set")
|
|
|
|
if dataset_fn is not None:
|
|
if input_workers.num_workers != len(input_contexts):
|
|
raise ValueError(
|
|
"Number of input workers (%d) is not same as number of "
|
|
"input_contexts (%d)" %
|
|
(input_workers.num_workers, len(input_contexts)))
|
|
self._input_contexts = input_contexts
|
|
self._num_replicas_in_sync = self._input_contexts[0].num_replicas_in_sync
|
|
self._dataset_fn = dataset_fn
|
|
self._built = False
|
|
if build:
|
|
self.build()
|
|
else:
|
|
if element_spec is None:
|
|
raise ValueError(
|
|
"element_spec should also be passed when passing components")
|
|
if not build:
|
|
raise ValueError(
|
|
"When constructing DistributedDatasetFromFunction with components, "
|
|
"build should not be False. This is an internal error. Please file "
|
|
"a bug.")
|
|
self._element_spec = element_spec
|
|
self._datasets = components
|
|
self._num_replicas_in_sync = None
|
|
self._built = True
|
|
self._cardinality = _cardinality(self._datasets[0])
|
|
self._enable_get_next_as_optional = _enable_get_next_as_optional(
|
|
self._strategy, self._datasets[0], self._cardinality)
|
|
|
|
def build(self):
|
|
assert not self._built
|
|
distribute_start_time_ns = time.time_ns()
|
|
self._datasets, element_spec = (
|
|
_create_datasets_from_function_with_input_context(
|
|
self._input_contexts, self._input_workers, self._dataset_fn))
|
|
if context.executing_eagerly():
|
|
# Records the time to initialize the distributed dataset.
|
|
context.async_wait()
|
|
distribute_duration_ms = (time.time_ns() -
|
|
distribute_start_time_ns) // 1_000_000
|
|
_distributed_dataset_from_function_initialization_time_milliseconds.get_cell(
|
|
self._strategy.__class__.__name__,
|
|
str(self._input_workers.num_workers)).add(distribute_duration_ms)
|
|
|
|
self._element_spec = _create_distributed_tensor_spec(
|
|
self._strategy, element_spec)
|
|
self._cardinality = _cardinality(self._datasets[0])
|
|
self._enable_get_next_as_optional = _enable_get_next_as_optional(
|
|
self._strategy, self._datasets[0], self._cardinality)
|
|
self._built = True
|
|
|
|
def auto_shard(self, num_shards, shard_ix):
|
|
assert (
|
|
len(self._datasets) == len(self._input_workers.worker_devices)
|
|
), (
|
|
f"datasets: {len(self._datasets)}, "
|
|
f"input workers: {len(self._input_workers.worker_devices)}"
|
|
)
|
|
sharded_datasets = []
|
|
for i in range(len(self._input_workers.worker_devices)):
|
|
with ops.colocate_with(self._datasets[i]._variant_tensor): # pylint: disable=protected-access
|
|
sharded_datasets.append(
|
|
input_ops.auto_shard_dataset(
|
|
self._datasets[i], num_shards, shard_ix,
|
|
self._num_replicas_in_sync
|
|
)
|
|
)
|
|
return DistributedDatasetsFromFunction(self._input_workers, self._strategy,
|
|
components=sharded_datasets,
|
|
element_spec=self._element_spec,
|
|
options=self._options)
|
|
|
|
@property
|
|
def cardinality(self):
|
|
if not self._built:
|
|
raise ValueError(
|
|
"Cannot get the cardinality of a dataset that is not built")
|
|
return self._cardinality
|
|
|
|
def __iter__(self):
|
|
if not (ops.executing_eagerly_outside_functions() or
|
|
ops.get_default_graph().building_function):
|
|
raise RuntimeError("__iter__() is only supported inside of tf.function "
|
|
"or when eager execution is enabled.")
|
|
|
|
if not self._built:
|
|
raise ValueError("You need to use this dataset in "
|
|
"ClusterCoordinator.create_per_worker_dataset.")
|
|
|
|
canonicalize_devices = getattr(self._strategy, "_canonicalize_devices",
|
|
True)
|
|
|
|
iterators = _create_iterators_per_worker(
|
|
self._datasets,
|
|
self._input_workers,
|
|
options=self._options,
|
|
canonicalize_devices=canonicalize_devices)
|
|
iterator = DistributedIterator(
|
|
input_workers=self._input_workers,
|
|
iterators=iterators,
|
|
strategy=self._strategy,
|
|
cardinality=self._cardinality,
|
|
enable_get_next_as_optional=self._enable_get_next_as_optional,
|
|
options=self._options,
|
|
replica_order=self._replica_order,
|
|
)
|
|
iterator._element_spec = self._element_spec # pylint: disable=protected-access
|
|
|
|
# When async eager is enabled, sometimes the iterator may not finish
|
|
# initialization before passing to a multi device function, add a sync
|
|
# point here to make sure all underlying iterators are initialized.
|
|
if context.executing_eagerly():
|
|
context.async_wait()
|
|
|
|
return iterator
|
|
|
|
@property
|
|
def element_spec(self):
|
|
"""The type specification of an element of this dataset."""
|
|
# When partial batch handling is enabled, always set the batch dimension to
|
|
# None, otherwise we just follow element_spec of the underlying dataset
|
|
# (whose batch dimension may also be None). This is because with partial
|
|
# batching handling we could always produce empty batches.
|
|
if (self._enable_get_next_as_optional and
|
|
self._strategy.extended._in_multi_worker_mode()): # pylint: disable=protected-access
|
|
return nest.map_structure(
|
|
_rebatch_as_dynamic, self._element_spec, expand_composites=False)
|
|
return self._element_spec
|
|
|
|
@property
|
|
def _type_spec(self):
|
|
return DistributedDatasetsFromFunctionSpec(self._input_workers,
|
|
self._element_spec,
|
|
self._strategy, self._options)
|
|
|
|
|
|
def _dummy_tensor_fn(value_structure):
|
|
"""A function to create dummy tensors from `value_structure`."""
|
|
|
|
def create_dummy_tensor(spec):
|
|
"""Create a dummy tensor with possible batch dimensions set to 0."""
|
|
if hasattr(spec, "_create_empty_value"):
|
|
# Type spec may overwrite default dummy values behavior by declaring the
|
|
# `_create_empty_value(self)` method. This method must return a value
|
|
# compatible with the type spec with batch dimensions set to 0 or fail if
|
|
# such a value does not exist. This allows a composite tensor to customize
|
|
# dummy values creation as, in general, its dummy value is not composed
|
|
# from dummy components (e.g. `row_splits` tensor of a RaggedTensor is
|
|
# never allowed to be empty). See b/183969859 for more discussions.
|
|
# TODO(b/186079336): reconsider CompositeTensor support.
|
|
return spec._create_empty_value() # pylint: disable=protected-access
|
|
|
|
if isinstance(spec, ragged_tensor.RaggedTensorSpec):
|
|
# Splice out the ragged dimensions.
|
|
# pylint: disable=protected-access
|
|
feature_shape = spec._shape[:1].concatenate(
|
|
spec._shape[(1 + spec._ragged_rank):])
|
|
feature_type = spec._dtype
|
|
# pylint: enable=protected-access
|
|
else:
|
|
feature_shape = spec.shape
|
|
feature_type = spec.dtype
|
|
# Ideally we should set the batch dimension to 0, however as in
|
|
# DistributionStrategy we don't know the batch dimension, we try to
|
|
# guess it as much as possible. If the feature has unknown dimensions, we
|
|
# will set them to 0. If the feature shape is already static, we guess the
|
|
# first dimension as batch dimension and set it to 0.
|
|
dims = ([dim if dim is not None else 0 for dim in feature_shape.as_list()]
|
|
if feature_shape else [])
|
|
if dims and (isinstance(spec, ragged_tensor.RaggedTensorSpec) or
|
|
feature_shape.is_fully_defined()):
|
|
dims[0] = tensor_shape.Dimension(0)
|
|
|
|
if isinstance(spec, sparse_tensor.SparseTensorSpec):
|
|
return sparse_tensor.SparseTensor(
|
|
values=array_ops.zeros(0, feature_type),
|
|
indices=array_ops.zeros((0, len(dims)), dtypes.int64),
|
|
dense_shape=dims)
|
|
|
|
# Create the dummy tensor.
|
|
dummy_tensor = array_ops.zeros(tensor_shape.TensorShape(dims), feature_type)
|
|
if isinstance(spec, ragged_tensor.RaggedTensorSpec):
|
|
# Reinsert the ragged dimensions with size 0.
|
|
# pylint: disable=protected-access
|
|
row_splits = array_ops.zeros(1, spec._row_splits_dtype)
|
|
dummy_tensor = ragged_tensor.RaggedTensor.from_nested_row_splits(
|
|
dummy_tensor, (row_splits,) * spec._ragged_rank, validate=False)
|
|
# pylint: enable=protected-access
|
|
return dummy_tensor
|
|
|
|
return nest.map_structure(create_dummy_tensor, value_structure)
|
|
|
|
|
|
def _get_value_or_dummy(input_workers, optional_list, produce_dummy):
|
|
"""Returns the value of the optionals or dummy values.
|
|
|
|
Args:
|
|
input_workers: the `InputWorkers`.
|
|
optional_list: a list of lists `tf.experimental.Optional`. The values from
|
|
each compute device grouped by the input device.
|
|
produce_dummy: a bool. Whether to produce dummy tensors when the optional
|
|
doesn't have a value.
|
|
|
|
Returns:
|
|
A flatten list of Tensors.
|
|
|
|
"""
|
|
value_list = []
|
|
for i, worker in enumerate(input_workers.worker_devices):
|
|
with ops.device(worker):
|
|
devices = input_workers.compute_devices_for_worker(i)
|
|
for j, device in enumerate(devices):
|
|
with ops.device(device):
|
|
if produce_dummy:
|
|
# pylint: disable=cell-var-from-loop
|
|
value_list.append(
|
|
tf_cond.cond(
|
|
optional_list[i][j].has_value(),
|
|
lambda: optional_list[i][j].get_value(), # pylint: disable=unnecessary-lambda
|
|
lambda: _dummy_tensor_fn(optional_list[i][j].element_spec),
|
|
strict=True,
|
|
))
|
|
# pylint: enable=cell-var-from-loop
|
|
else:
|
|
value_list.append(optional_list[i][j].get_value())
|
|
return value_list
|
|
|
|
|
|
class _SingleWorkerDatasetIteratorBase(object):
|
|
"""Iterator for a single `tf.data.Dataset`."""
|
|
|
|
def __init__(self, dataset, worker, devices, options=None):
|
|
"""Create iterator for the `dataset` to fetch data to worker's `devices` .
|
|
|
|
A `MultiDeviceIterator` or `OwnedMultiDeviceIterator` is used to prefetch
|
|
input to the devices on the given worker.
|
|
|
|
Args:
|
|
dataset: A `tf.data.Dataset` instance.
|
|
worker: Worker on which ops should be created.
|
|
devices: Distribute data from `dataset` to these devices.
|
|
options: options.
|
|
"""
|
|
self._dataset = dataset
|
|
self._worker = worker
|
|
self._devices = devices
|
|
self._element_spec = dataset.element_spec
|
|
self._options = options
|
|
self._make_iterator()
|
|
|
|
def _make_iterator(self):
|
|
raise NotImplementedError("must be implemented in descendants")
|
|
|
|
def _format_data_list_with_options(self, data_list):
|
|
"""Change the data in to a list type if required.
|
|
|
|
The OwnedMultiDeviceIterator returns the list data type,
|
|
while the PER_REPLICA iterator (when used with prefetch disabled)
|
|
returns without the enclosed list. This is to fix the inconsistency.
|
|
Args:
|
|
data_list: data_list
|
|
Returns:
|
|
list
|
|
"""
|
|
if (self._options and self._options.experimental_replication_mode ==
|
|
InputReplicationMode.PER_REPLICA and
|
|
not self._options.experimental_fetch_to_device):
|
|
return [data_list]
|
|
else:
|
|
return data_list
|
|
|
|
def get_next(self, device, name=None):
|
|
"""Get next element for the given device."""
|
|
del name
|
|
with ops.device(self._worker):
|
|
if _should_use_multi_device_iterator(self._options):
|
|
return self._iterator.get_next(device)
|
|
else:
|
|
return self._iterator.get_next()
|
|
|
|
def get_next_as_list(self, name=None):
|
|
"""Get next element from the underlying iterator.
|
|
|
|
Runs the iterator get_next() within a device scope. Since this doesn't use
|
|
get_next_as_optional(), it is considerably faster than get_next_as_list(),
|
|
but it raises EOFError if any of the device doesn't get any data.
|
|
|
|
Args:
|
|
name: not used.
|
|
|
|
Returns:
|
|
A list consisting of the next data from each device.
|
|
"""
|
|
del name
|
|
with ops.device(self._worker):
|
|
return self._format_data_list_with_options(self._iterator.get_next())
|
|
|
|
def get_next_as_optional_list(self):
|
|
with ops.device(self._worker):
|
|
return self._format_data_list_with_options(
|
|
self._iterator.get_next_as_optional())
|
|
|
|
|
|
class _SingleWorkerDatasetIteratorSpec(type_spec.TypeSpec):
|
|
"""Type specification for `_SingleWorkerOwnedDatasetIterator`."""
|
|
|
|
__slots__ = [
|
|
"_worker", "_devices", "_element_spec", "_options",
|
|
"_canonicalize_devices"
|
|
]
|
|
|
|
def __init__(self, worker, devices, element_spec, options,
|
|
canonicalize_devices=True):
|
|
self._worker = worker
|
|
if canonicalize_devices:
|
|
self._devices = tuple(device_util.canonicalize(d) for d in devices)
|
|
else:
|
|
self._devices = tuple(
|
|
device_util.canonicalize_without_job_and_task(d) for d in devices)
|
|
self._element_spec = element_spec
|
|
# `self._options` intentionally made not `None` for proper serialization.
|
|
self._options = (options if options is not None else
|
|
distribute_lib.InputOptions())
|
|
self._canonicalize_devices = canonicalize_devices
|
|
|
|
@property
|
|
def value_type(self):
|
|
return _SingleWorkerOwnedDatasetIterator
|
|
|
|
def _serialize(self):
|
|
return (self._worker, self._devices, self._element_spec, self._options,
|
|
self._canonicalize_devices)
|
|
|
|
def _get_multi_device_iterator_spec(self, specs):
|
|
device_scope = device_util.canonicalize(self._worker, device_util.current())
|
|
host_device = device_util.get_host_for_device(device_scope)
|
|
# source_device while creating iterator governs the worker device in
|
|
# iterator spec.
|
|
worker = host_device
|
|
specs.append(
|
|
multi_device_iterator_ops.MultiDeviceIteratorSpec(
|
|
self._devices, worker, element_spec=self._element_spec))
|
|
|
|
@property
|
|
def _component_specs(self):
|
|
specs = []
|
|
if _should_use_multi_device_iterator(self._options):
|
|
self._get_multi_device_iterator_spec(specs)
|
|
else:
|
|
specs.append(iterator_ops.IteratorSpec(element_spec=self._element_spec))
|
|
return specs
|
|
|
|
def _to_components(self, value):
|
|
return [value._iterator] # pylint: disable=protected-access
|
|
|
|
def _from_components(self, components):
|
|
return _SingleWorkerOwnedDatasetIterator(
|
|
dataset=None,
|
|
worker=self._worker,
|
|
devices=self._devices,
|
|
components=components,
|
|
element_spec=self._element_spec,
|
|
options=self._options,
|
|
canonicalize_devices=self._canonicalize_devices)
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
# pylint: disable=protected-access
|
|
return _SingleWorkerDatasetIteratorSpec(value._worker, value._devices,
|
|
value._element_spec, value._options,
|
|
value._canonicalize_devices)
|
|
|
|
|
|
class _SingleWorkerOwnedDatasetIterator(_SingleWorkerDatasetIteratorBase,
|
|
composite_tensor.CompositeTensor):
|
|
"""Iterator for a DistributedDataset instance."""
|
|
|
|
def __init__(self,
|
|
dataset=None,
|
|
worker=None,
|
|
devices=None,
|
|
components=None,
|
|
element_spec=None,
|
|
options=None,
|
|
canonicalize_devices=None):
|
|
"""Create iterator for the `dataset` to fetch data to worker's `devices` .
|
|
|
|
`OwnedMultiDeviceIterator` is used to prefetch input to the devices on the
|
|
given worker. The lifetime of this iterator is tied to the encompassing
|
|
python object. Once we go out of scope of the python object or return from
|
|
a tf.function the underlying iterator resource is deleted.
|
|
|
|
Args:
|
|
dataset: A `tf.data.Dataset` instance.
|
|
worker: Worker on which ops should be created.
|
|
devices: Distribute data from `dataset` to these devices.
|
|
components: Tensor components to construct the
|
|
_SingleWorkerOwnedDatasetIterator from.
|
|
element_spec: A nested structure of `TypeSpec` objects that represents the
|
|
type specification of elements of the iterator.
|
|
options: `tf.distribute.InputOptions` used to control options on how this
|
|
dataset is distributed.
|
|
canonicalize_devices: Whether to canonicalize devices for workers fully or
|
|
partially. If False, it will partially canonicalize devices by removing
|
|
job and task.
|
|
"""
|
|
if worker is None or devices is None:
|
|
raise ValueError("Both `worker` and `devices` should be provided")
|
|
|
|
error_message = ("Either `dataset` or both `components` and `element_spec` "
|
|
"need to be provided.")
|
|
|
|
self._options = options
|
|
self._canonicalize_devices = canonicalize_devices
|
|
if dataset is None:
|
|
if (components is None or element_spec is None):
|
|
raise ValueError(error_message)
|
|
self._element_spec = element_spec
|
|
self._worker = worker
|
|
self._devices = devices
|
|
self._iterator = components[0]
|
|
else:
|
|
if (components is not None or element_spec is not None):
|
|
raise ValueError(error_message)
|
|
super(_SingleWorkerOwnedDatasetIterator,
|
|
self).__init__(dataset, worker, devices, self._options)
|
|
|
|
def _create_owned_multi_device_iterator(self):
|
|
# If the worker devices are already canonicalized, canonicalizing again
|
|
# would have no impact.
|
|
# For strategies running on remote workers such as PS Strategy, the device
|
|
# scope will be derived from current worker, if used under init_scope().
|
|
if not ops.inside_function():
|
|
device_scope = device_util.canonicalize(self._worker,
|
|
device_util.current())
|
|
host_device = device_util.get_host_for_device(device_scope)
|
|
else:
|
|
# In general, iterators should not be created within tf.functions. For
|
|
# exact visitation guarantee solutions for parameter server training,
|
|
# however, we do create iterators within the tf.functions that are
|
|
# dispatched to workers. In these cases, the traced device must match the
|
|
# runtime device. Since tracing occurs on the chief, we do not want to use
|
|
# the current device scope, which would be the chief, but rather use the
|
|
# relative worker device scope explicitly.
|
|
device_scope, host_device = self._worker, self._worker
|
|
with ops.device(device_scope):
|
|
if self._options is not None:
|
|
self._iterator = multi_device_iterator_ops.OwnedMultiDeviceIterator(
|
|
self._dataset,
|
|
self._devices,
|
|
source_device=host_device,
|
|
max_buffer_size=self._options
|
|
.experimental_per_replica_buffer_size,
|
|
prefetch_buffer_size=self._options
|
|
.experimental_per_replica_buffer_size)
|
|
else:
|
|
self._iterator = multi_device_iterator_ops.OwnedMultiDeviceIterator(
|
|
self._dataset, self._devices, source_device=host_device)
|
|
|
|
def _make_iterator(self):
|
|
"""Make appropriate iterator on the dataset."""
|
|
if not self._worker:
|
|
raise ValueError("Worker device must be specified when creating an "
|
|
"owned iterator.")
|
|
if _should_use_multi_device_iterator(self._options):
|
|
self._create_owned_multi_device_iterator()
|
|
else:
|
|
with ops.device(self._worker):
|
|
self._iterator = iter(self._dataset)
|
|
|
|
@property
|
|
def element_spec(self):
|
|
return self._element_spec
|
|
|
|
@property
|
|
def _type_spec(self):
|
|
return _SingleWorkerDatasetIteratorSpec(self._worker, self._devices,
|
|
self._element_spec, self._options,
|
|
self._canonicalize_devices)
|
|
|
|
@property
|
|
def output_classes(self):
|
|
"""Returns the class of each component of an element of this iterator.
|
|
|
|
The expected values are `tf.Tensor` and `tf.SparseTensor`.
|
|
|
|
Returns:
|
|
A nested structure of Python `type` objects corresponding to each
|
|
component of an element of this dataset.
|
|
"""
|
|
return nest.map_structure(
|
|
lambda component_spec: component_spec._to_legacy_output_classes(), # pylint: disable=protected-access
|
|
self._element_spec)
|
|
|
|
@property
|
|
def output_shapes(self):
|
|
"""Returns the shape of each component of an element of this iterator.
|
|
|
|
Returns:
|
|
A nested structure of `tf.TensorShape` objects corresponding to each
|
|
component of an element of this dataset.
|
|
"""
|
|
return nest.map_structure(
|
|
lambda component_spec: component_spec._to_legacy_output_shapes(), # pylint: disable=protected-access
|
|
self._element_spec)
|
|
|
|
@property
|
|
def output_types(self):
|
|
"""Returns the type of each component of an element of this iterator.
|
|
|
|
Returns:
|
|
A nested structure of `tf.DType` objects corresponding to each component
|
|
of an element of this dataset.
|
|
"""
|
|
return nest.map_structure(
|
|
lambda component_spec: component_spec._to_legacy_output_types(), # pylint: disable=protected-access
|
|
self._element_spec)
|
|
|
|
|
|
def _create_iterators_per_worker(worker_datasets,
|
|
input_workers,
|
|
options=None,
|
|
canonicalize_devices=False):
|
|
"""Create a multidevice iterator on each of the workers."""
|
|
assert isinstance(input_workers, InputWorkers)
|
|
assert len(worker_datasets) == len(input_workers.worker_devices)
|
|
iterators = []
|
|
for i, worker in enumerate(input_workers.worker_devices):
|
|
with ops.device(worker):
|
|
worker_devices = input_workers.compute_devices_for_worker(i)
|
|
iterator = _SingleWorkerOwnedDatasetIterator(
|
|
dataset=worker_datasets[i],
|
|
worker=worker,
|
|
devices=worker_devices,
|
|
options=options,
|
|
canonicalize_devices=canonicalize_devices)
|
|
iterators.append(iterator)
|
|
return iterators
|
|
|
|
|
|
def _create_datasets_from_function_with_input_context(input_contexts,
|
|
input_workers,
|
|
dataset_fn):
|
|
"""Create device datasets per worker given a dataset function."""
|
|
datasets = []
|
|
for i, ctx in enumerate(input_contexts):
|
|
worker = input_workers.worker_devices[i]
|
|
with ops.device(worker):
|
|
dataset = dataset_fn(ctx)
|
|
datasets.append(dataset)
|
|
return datasets, dataset.element_spec
|
|
|
|
|
|
# TODO(sourabhbajaj): Remove this in lieu of distributed datasets
|
|
def _get_batched_dataset(d):
|
|
"""Get the batched dataset from `d`."""
|
|
# pylint: disable=protected-access
|
|
if isinstance(d, dataset_ops.DatasetV1Adapter):
|
|
d = d._dataset
|
|
|
|
if isinstance(d, (dataset_ops.BatchDataset, batching._MapAndBatchDataset)):
|
|
return d
|
|
elif isinstance(d, (dataset_ops.PrefetchDataset,
|
|
dataset_ops._OptionsDataset)):
|
|
return _get_batched_dataset(d._input_dataset)
|
|
|
|
raise ValueError(
|
|
"Unable to get batched dataset from the input dataset. `batch` "
|
|
"`map_and_batch` need to be the last operations on the dataset. "
|
|
"The batch operations can be followed by a prefetch.")
|
|
|
|
|
|
def _get_batched_dataset_attributes(d):
|
|
"""Get `batch_size`, `drop_remainder` of dataset."""
|
|
# pylint: disable=protected-access
|
|
assert isinstance(d,
|
|
(dataset_ops.BatchDataset, batching._MapAndBatchDataset))
|
|
if isinstance(d, dataset_ops.BatchDataset):
|
|
batch_size = d._batch_size
|
|
drop_remainder = d._drop_remainder
|
|
elif isinstance(d, batching._MapAndBatchDataset):
|
|
batch_size = d._batch_size_t
|
|
drop_remainder = d._drop_remainder_t
|
|
# pylint: enable=protected-access
|
|
|
|
if tensor_util.is_tf_type(batch_size):
|
|
batch_size = tensor_util.constant_value(batch_size)
|
|
|
|
if tensor_util.is_tf_type(drop_remainder):
|
|
drop_remainder = tensor_util.constant_value(drop_remainder)
|
|
|
|
return batch_size, drop_remainder
|
|
|
|
|
|
# TODO(sourabhbajaj): Remove this in lieu of distributed datasets
|
|
def _get_dataset_attributes(dataset):
|
|
"""Get the underlying attributes from the dataset object."""
|
|
# pylint: disable=protected-access
|
|
|
|
# First, get batch_size and drop_remainder from the dataset. We need
|
|
# to walk back the dataset creation process and find the batched version in
|
|
# order to get the attributes.
|
|
batched_dataset = _get_batched_dataset(dataset)
|
|
batch_size, drop_remainder = _get_batched_dataset_attributes(batched_dataset)
|
|
|
|
# Second, prefetch buffer should be get from the original dataset.
|
|
prefetch_buffer = None
|
|
if isinstance(dataset, dataset_ops.PrefetchDataset):
|
|
prefetch_buffer = dataset._buffer_size
|
|
elif (isinstance(dataset, dataset_ops.DatasetV1Adapter)
|
|
and isinstance(dataset._dataset, dataset_ops.PrefetchDataset)):
|
|
prefetch_buffer = dataset._dataset._buffer_size
|
|
|
|
return batch_size, drop_remainder, prefetch_buffer
|
|
|
|
|
|
def _should_use_multi_device_iterator(options):
|
|
"""Determine whether to use multi_device_iterator_ops."""
|
|
if (options is None or
|
|
options.experimental_replication_mode == InputReplicationMode.PER_WORKER
|
|
or
|
|
(options.experimental_replication_mode == InputReplicationMode.PER_REPLICA
|
|
and options.experimental_fetch_to_device)):
|
|
return True
|
|
return False
|
|
|
|
|
|
class MultiStepContext(object):
|
|
"""A context object that can be used to capture things when running steps.
|
|
|
|
This context object is useful when running multiple steps at a time using the
|
|
`experimental_run_steps_on_iterator` API. For e.g. it allows the user's step
|
|
function to specify which outputs to emit at what frequency. Currently it
|
|
supports capturing output from the last step, as well as capturing non tensor
|
|
outputs. In the future it will be augmented to support other use cases such
|
|
as output each N steps.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize an output context.
|
|
|
|
Returns:
|
|
A context object.
|
|
"""
|
|
self._last_step_outputs = {}
|
|
self._last_step_outputs_reduce_ops = {}
|
|
self._non_tensor_outputs = {}
|
|
|
|
@property
|
|
def last_step_outputs(self):
|
|
"""A dictionary consisting of outputs to be captured on last step.
|
|
|
|
Keys in the dictionary are names of tensors to be captured, as specified
|
|
when `set_last_step_output` is called.
|
|
Values in the dictionary are the tensors themselves. If
|
|
`set_last_step_output` was called with a `reduce_op` for this output,
|
|
then the value is the reduced value.
|
|
|
|
Returns:
|
|
A dictionary with last step outputs.
|
|
"""
|
|
return self._last_step_outputs
|
|
|
|
def _set_last_step_outputs(self, outputs):
|
|
"""Replace the entire dictionary of last step outputs."""
|
|
if not isinstance(outputs, dict):
|
|
raise ValueError("Need a dictionary to set last_step_outputs.")
|
|
self._last_step_outputs = outputs
|
|
|
|
def set_last_step_output(self, name, output, reduce_op=None):
|
|
"""Set `output` with `name` to be outputted from the last step.
|
|
|
|
Args:
|
|
name: String, name to identify the output. Doesn't need to match tensor
|
|
name.
|
|
output: The tensors that should be outputted with `name`. See below for
|
|
actual types supported.
|
|
reduce_op: Reduction method to use to reduce outputs from multiple
|
|
replicas. Required if `set_last_step_output` is called in a replica
|
|
context. Optional in cross_replica_context.
|
|
When present, the outputs from all the replicas are reduced using the
|
|
current distribution strategy's `reduce` method. Hence, the type of
|
|
`output` must be what's supported by the corresponding `reduce` method.
|
|
For e.g. if using MirroredStrategy and reduction is set, output
|
|
must be a `PerReplica` value.
|
|
The reduce method is also recorded in a dictionary
|
|
`_last_step_outputs_reduce_ops` for later interpreting of the
|
|
outputs as already reduced or not.
|
|
"""
|
|
if distribute_lib.in_cross_replica_context():
|
|
self._last_step_outputs_reduce_ops[name] = reduce_op
|
|
if reduce_op is None:
|
|
self._last_step_outputs[name] = output
|
|
else:
|
|
distribution = distribute_lib.get_strategy()
|
|
self._last_step_outputs[name] = distribution.reduce(reduce_op, output,
|
|
axis=None)
|
|
else:
|
|
assert reduce_op is not None
|
|
def merge_fn(distribution, value):
|
|
self._last_step_outputs[name] = distribution.reduce(reduce_op, value,
|
|
axis=None)
|
|
# Setting this inside the `merge_fn` because all replicas share the same
|
|
# context object, so it's more robust to set it only once (even if all
|
|
# the replicas are trying to set the same value).
|
|
self._last_step_outputs_reduce_ops[name] = reduce_op
|
|
|
|
distribute_lib.get_replica_context().merge_call(
|
|
merge_fn, args=(output,))
|
|
|
|
@property
|
|
def non_tensor_outputs(self):
|
|
"""A dictionary consisting of any non tensor outputs to be captured."""
|
|
return self._non_tensor_outputs
|
|
|
|
def set_non_tensor_output(self, name, output):
|
|
"""Set `output` with `name` to be captured as a non tensor output."""
|
|
if distribute_lib.in_cross_replica_context():
|
|
self._non_tensor_outputs[name] = output
|
|
else:
|
|
def merge_fn(distribution, value):
|
|
# NOTE(priyag): For non tensor outputs, we simply return all the values
|
|
# in a list as reduction doesn't make sense on non tensors.
|
|
self._non_tensor_outputs[name] = (
|
|
distribution.experimental_local_results(value))
|
|
distribute_lib.get_replica_context().merge_call(
|
|
merge_fn, args=(output,))
|
|
|
|
|
|
def _create_distributed_tensor_spec(strategy, tensor_spec):
|
|
"""Create a `tf.TypeSpec` for a given strategy and input `tensor_spec`.
|
|
|
|
Args:
|
|
strategy: The given `tf.distribute` strategy.
|
|
tensor_spec: `tf.TensorSpec` of a given value. The batch dimension of the
|
|
shape should be None if you have partial batches.
|
|
|
|
Returns:
|
|
A `tf.TypeSpec` that matches the values produced by a given strategy. This
|
|
can be a `tf.TensorSpec` or a `PerRelicaSpec`.
|
|
"""
|
|
num_replicas = len(strategy.extended.worker_devices)
|
|
|
|
# For one device strategy that is not MultiWorkerMirroredStrategy, return the
|
|
# tensor_spec as is, since we don't wrap the output with PerReplica in this
|
|
# case.
|
|
# TODO(b/166464552): remove after we always wrap for all strategies.
|
|
if not _always_wrap(strategy):
|
|
return tensor_spec
|
|
|
|
# For other cases we assume the input to tf.function is a per replica type.
|
|
def _get_value_per_replica(tensor_spec_per_input):
|
|
value_specs = [tensor_spec_per_input for _ in range(num_replicas)]
|
|
return values.PerReplicaSpec(*value_specs)
|
|
|
|
return nest.map_structure(_get_value_per_replica, tensor_spec)
|
|
|
|
|
|
def _replace_per_replica_spec(spec, i):
|
|
"""If `spec` is a `PerReplicaSpec`, then return its `i`th value_spec."""
|
|
if isinstance(spec, values.PerReplicaSpec):
|
|
return spec._value_specs[i] # pylint: disable=protected-access
|
|
else:
|
|
return spec
|
|
|
|
|
|
def _cardinality(dataset):
|
|
"""Returns the cardinality of the dataset."""
|
|
if context.executing_eagerly():
|
|
with ops.device(dataset._variant_tensor.device): # pylint: disable=protected-access
|
|
return dataset.cardinality().numpy()
|
|
return cardinality_lib.UNKNOWN
|
|
|
|
|
|
def _enable_get_next_as_optional(strategy, dataset, cardinality):
|
|
"""Returns whether to enable using partial batch handling."""
|
|
# TODO(b/133073708): we currently need a flag to control the usage because
|
|
# there is a performance difference between get_next() and
|
|
# get_next_as_optional(). And we only enable get_next_as_optional when the
|
|
# output shapes are not static.
|
|
#
|
|
# TODO(rxsang): We want to always enable the get_next_as_optional behavior
|
|
# when user passed input_fn instead of dataset.
|
|
if not getattr(
|
|
strategy.extended, "enable_partial_batch_handling",
|
|
getattr(strategy.extended, "experimental_enable_get_next_as_optional",
|
|
False)):
|
|
return False
|
|
|
|
# If the dataset is infinite, we don't need to enable last partial batch
|
|
# support. Note that we can only evaluate the cardinality of the dataset in
|
|
# eager.
|
|
if cardinality == cardinality_lib.INFINITE:
|
|
return False
|
|
|
|
return not _is_statically_shaped(
|
|
dataset.element_spec) or strategy.extended._in_multi_worker_mode() # pylint: disable=protected-access
|
|
|
|
|
|
def _create_per_replica(value_list, strategy):
|
|
"""Creates a PerReplica.
|
|
|
|
For strategies other than OneDeviceStrategy, it creates a PerReplica whose
|
|
type spec is set to the element spec of the dataset. This helps avoid
|
|
retracing for partial batches. Retracing is problematic for multi client when
|
|
different client retraces different time, since retracing changes the
|
|
collective keys in the tf.function, and causes mismatches among clients.
|
|
|
|
For single client strategies, this simply calls distribute_utils.regroup().
|
|
|
|
Args:
|
|
value_list: a list of values, one for each replica.
|
|
strategy: the `tf.distribute.Strategy`.
|
|
|
|
Returns:
|
|
a structure of PerReplica.
|
|
|
|
"""
|
|
# TODO(b/166464552): always wrap for all one device strategies as well.
|
|
always_wrap = _always_wrap(strategy)
|
|
per_replicas = distribute_utils.regroup(value_list, always_wrap=always_wrap)
|
|
return per_replicas
|
|
|
|
|
|
def _always_wrap(strategy):
|
|
"""Returns whether to always wrap the values in a DistributedValues."""
|
|
return strategy.extended._in_multi_worker_mode() or len( # pylint: disable=protected-access
|
|
strategy.extended.worker_devices) > 1
|
|
|
|
|
|
def _rebatch_as_dynamic(per_replica_spec):
|
|
"""Rebatch the spec to have a dynamic batch dimension."""
|
|
assert isinstance(per_replica_spec, values.PerReplicaSpec), per_replica_spec
|
|
|
|
# pylint: disable=protected-access
|
|
def _rebatch(spec):
|
|
# Rebatch if possible.
|
|
try:
|
|
return spec._unbatch()._batch(None)
|
|
except ValueError:
|
|
pass
|
|
return spec
|
|
|
|
return values.PerReplicaSpec(
|
|
*nest.map_structure(_rebatch, per_replica_spec._value_specs))
|
|
# pylint: enable=protected-access
|
|
|
|
|
|
def _ag_enumerate_not_implemented(s, unused_start):
|
|
msg = (
|
|
f"enumerate not supported with {s.__class__.__name__} types within "
|
|
"tf.functions. Use a for loop over the dataset and keep a separate "
|
|
"counter instead."
|
|
)
|
|
raise NotImplementedError(msg)
|
|
|
|
|
|
py_builtins.enumerate_registry.register(
|
|
DistributedIterator, _ag_enumerate_not_implemented
|
|
)
|
|
py_builtins.enumerate_registry.register(
|
|
DistributedDataset, _ag_enumerate_not_implemented
|
|
)
|