2322 lines
89 KiB
Python
2322 lines
89 KiB
Python
# Copyright 2023 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.
|
|
# ==============================================================================
|
|
"""Mid level API for TPU Embeddings With V2 Embedding Accelerator."""
|
|
|
|
import collections
|
|
import copy
|
|
import dataclasses
|
|
import functools
|
|
import hashlib
|
|
import operator
|
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
|
|
|
from absl import logging
|
|
|
|
from tensorflow.core.framework import attr_value_pb2
|
|
from tensorflow.core.tpu.kernels import sparse_core_layout_pb2
|
|
from tensorflow.python.checkpoint import saveable_compat
|
|
from tensorflow.python.distribute import device_util
|
|
from tensorflow.python.distribute import distribute_lib
|
|
from tensorflow.python.distribute import tpu_strategy
|
|
from tensorflow.python.distribute import tpu_util
|
|
from tensorflow.python.distribute import tpu_values
|
|
from tensorflow.python.distribute import values
|
|
from tensorflow.python.distribute import values_util
|
|
from tensorflow.python.eager import def_function
|
|
from tensorflow.python.framework import constant_op
|
|
from tensorflow.python.framework import device as tf_device
|
|
from tensorflow.python.framework import dtypes
|
|
from tensorflow.python.framework import ops
|
|
from tensorflow.python.framework import sparse_tensor
|
|
from tensorflow.python.framework import tensor
|
|
from tensorflow.python.framework import tensor_shape
|
|
from tensorflow.python.ops import array_ops
|
|
from tensorflow.python.ops import control_flow_ops
|
|
from tensorflow.python.ops import gen_resource_variable_ops
|
|
from tensorflow.python.ops import math_ops
|
|
from tensorflow.python.ops import summary_ops_v2
|
|
from tensorflow.python.ops import variable_scope
|
|
from tensorflow.python.ops import variables as tf_variables
|
|
from tensorflow.python.ops.ragged import ragged_tensor
|
|
from tensorflow.python.tpu import _pywrap_sparse_core_layout
|
|
from tensorflow.python.tpu import embedding_context_utils as ecu
|
|
from tensorflow.python.tpu import tpu_embedding_base
|
|
from tensorflow.python.tpu import tpu_embedding_v2_utils
|
|
from tensorflow.python.tpu import tpu_embedding_v3_checkpoint_adapter
|
|
from tensorflow.python.tpu import tpu_embedding_v3_utils
|
|
from tensorflow.python.tpu import tpu_replication
|
|
from tensorflow.python.tpu.ops import gen_xla_ops as xla_ops
|
|
from tensorflow.python.trackable import base
|
|
from tensorflow.python.training.saving import saveable_object
|
|
from tensorflow.python.util import compat
|
|
from tensorflow.python.util import nest
|
|
from tensorflow.python.util import tf_inspect
|
|
from tensorflow.python.util.tf_export import tf_export
|
|
|
|
|
|
_PIPELINE_ATTRIBUTE = "_embedding_pipelining"
|
|
_PIPELINE_MODE_FORWARD = "forward"
|
|
_PIPELINE_MODE_BACKWARD = "backward"
|
|
_PIPELINE_MODEL_SEQUENTIAL = "_sequential"
|
|
_PARAMETER_NAME = "parameters"
|
|
|
|
|
|
TableConfig = tpu_embedding_v2_utils.TableConfig
|
|
FeatureConfig = tpu_embedding_v2_utils.TableConfig
|
|
QuantizationConfig = tpu_embedding_v2_utils.QuantizationConfig
|
|
|
|
|
|
@tf_export("tpu.experimental.embedding.SparseCoreEmbeddingConfig")
|
|
@dataclasses.dataclass(frozen=True)
|
|
class SparseCoreEmbeddingConfig:
|
|
"""Config for sparsecore embedding."""
|
|
|
|
disable_table_stacking: bool = False
|
|
max_ids_per_chip_per_sample: int = 64
|
|
max_ids_per_table: Optional[Dict[str, int]] = None
|
|
max_unique_ids_per_table: Optional[Dict[str, int]] = None
|
|
allow_id_dropping: bool = False
|
|
initialize_tables_on_host: bool = True
|
|
enable_fast_table_initialization: bool = False
|
|
|
|
|
|
class EmbeddingPipeliningContext(control_flow_ops.ControlFlowContext):
|
|
"""Sets the _embedding_pipelining attribute on all ops created in the scope."""
|
|
|
|
def __init__(self, mode: str, enable: bool):
|
|
super().__init__()
|
|
self._name = "EmbeddingPipelinigContext"
|
|
self._mode = attr_value_pb2.AttrValue(s=compat.as_bytes(mode))
|
|
self._enable = enable
|
|
recording_summaries = summary_ops_v2.is_recording_summaries()
|
|
if not isinstance(recording_summaries, bool):
|
|
# We can't handle predicate functions at this point. So, we'll ignore the
|
|
# special casing of summary recording because, presumably, this is not
|
|
# a single step loop so pipelining is still valid.
|
|
recording_summaries = False
|
|
|
|
if enable and (
|
|
recording_summaries or not ecu.embedding_pipelining_state.enabled
|
|
):
|
|
# We'll still flag these ops for the SC forward/backward pass, but we'll
|
|
# run them sequentially. This has to be handled in the MLIR passes
|
|
# embedding_pipelining.cc and embedding_sequencing.cc.
|
|
disable_reason = (
|
|
"Summary recording"
|
|
if recording_summaries
|
|
else "_embedding_pipelining_state.enabled = False"
|
|
)
|
|
logging.info("%s detected, disabling pipelining.", disable_reason)
|
|
self._mode = attr_value_pb2.AttrValue(
|
|
s=compat.as_bytes(mode + _PIPELINE_MODEL_SEQUENTIAL)
|
|
)
|
|
|
|
def to_control_flow_context_def(
|
|
self, context_def: Any, export_scope: Any = None
|
|
):
|
|
# pylint: disable=useless-super-delegation
|
|
# The method is required by `ControlFlowContext`.
|
|
super().to_control_flow_context_def(context_def, export_scope)
|
|
|
|
def AddOp(self, op: ops.Operation):
|
|
# pylint: disable=protected-access
|
|
if self._enable:
|
|
op._set_attr(_PIPELINE_ATTRIBUTE, self._mode)
|
|
if self._outer_context:
|
|
self._outer_context.AddOp(op)
|
|
|
|
|
|
class TPUEmbeddingShardedSaveable(saveable_object.SaveableObject):
|
|
"""Defines how to save and restore a shard of TPUEmbedding sharded variable."""
|
|
|
|
def __init__(
|
|
self,
|
|
variable: tf_variables.Variable,
|
|
shard_id: int,
|
|
num_shards: int,
|
|
shard_dim: int,
|
|
name: str,
|
|
):
|
|
"""Init TPUEmbeddingShardedSaveable."""
|
|
self._shard_id = shard_id
|
|
self._variable = variable
|
|
|
|
var_offset = [0] * len(variable.shape)
|
|
# NOTE: always assume even sharding
|
|
var_offset[shard_dim] = shard_id * variable.shape[shard_dim]
|
|
fullshape = variable.shape.as_list()
|
|
fullshape[shard_dim] = num_shards * fullshape[shard_dim]
|
|
save_slice_info = tf_variables.Variable.SaveSliceInfo(
|
|
full_name=name,
|
|
full_shape=fullshape,
|
|
var_offset=var_offset,
|
|
var_shape=variable.shape.as_list(),
|
|
)
|
|
|
|
spec = saveable_object.SaveSpec(
|
|
tensor=variable.read_value,
|
|
slice_spec=save_slice_info.spec,
|
|
name=name,
|
|
dtype=variable.dtype,
|
|
device=variable.device,
|
|
)
|
|
super().__init__(variable.read_value, [spec], name)
|
|
|
|
def restore(
|
|
self,
|
|
restored_tensors: List[tensor.Tensor],
|
|
restored_shapes: List[tensor_shape.TensorShape],
|
|
) -> Any:
|
|
del restored_shapes
|
|
restored_tensor = restored_tensors[0]
|
|
|
|
return values_util.assign_on_device(
|
|
self._variable.device, self._variable, restored_tensor
|
|
)
|
|
|
|
|
|
def _fielddict():
|
|
return dataclasses.field(default_factory=dict)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class TableStacking:
|
|
"""Information about how we stack tables."""
|
|
|
|
# Indexed by stacked table name:
|
|
stacked_table_to_tables: Dict[str, TableConfig] = _fielddict()
|
|
quantization_configs: Dict[str, QuantizationConfig] = _fielddict()
|
|
|
|
# Indexed by table name:
|
|
table_name_to_table: Dict[str, TableConfig] = _fielddict()
|
|
table_to_padding_rows: Dict[str, int] = _fielddict()
|
|
table_to_padding_columns: Dict[str, int] = _fielddict()
|
|
table_to_sample_count: Dict[str, int] = _fielddict()
|
|
table_to_layout: Dict[str, sparse_core_layout_pb2.SparseCoreTableLayout] = (
|
|
_fielddict()
|
|
)
|
|
# Maps table name to (stacked table, row offset, shard rotation)
|
|
table_to_stacked_table_offset: Dict[str, Tuple[str, int, int]] = _fielddict()
|
|
|
|
# Indexed by feature_path the key of flat_features:
|
|
feature_to_sample_offset: Dict[str, int] = _fielddict()
|
|
|
|
|
|
@saveable_compat.legacy_saveable_name("")
|
|
class TPUEmbeddingShardedVariable(
|
|
tpu_values.TPUVariableMixin, values.DistributedVariable
|
|
):
|
|
"""A ShardedVariable class for Embedding tables on TPU."""
|
|
|
|
def _is_mirrored(self) -> bool:
|
|
return False
|
|
|
|
# Only support sharding on the first dimension.
|
|
@property
|
|
def shard_dim(self) -> int:
|
|
return 0
|
|
|
|
@property
|
|
def shape(self) -> tensor_shape.TensorShape:
|
|
"""Returns the shape of the embedding variable for the current context."""
|
|
local_shape = self._values[0].shape
|
|
global_shape = local_shape.as_list()
|
|
global_shape[self.shard_dim] = global_shape[self.shard_dim] * len(
|
|
self.values
|
|
)
|
|
return tensor_shape.TensorShape(global_shape)
|
|
|
|
def _write_object_proto(self, proto, options):
|
|
super()._write_object_proto(proto, options)
|
|
# TODO(b/305882915): Reset the saved model shape to the local shape
|
|
# for backward compatibility of users that directly access the full
|
|
# variable shape as the shape of values.
|
|
proto.variable.shape.CopyFrom(self._values[0].shape.as_proto())
|
|
|
|
def _gather_saveables_for_checkpoint(self) -> Dict[str, Callable[..., Any]]:
|
|
"""Overrides Trackable method.
|
|
|
|
Returns:
|
|
A dictionary mapping attribute names to `SaveableObject` factories.
|
|
"""
|
|
|
|
def _saveable_factory(name=self._common_name):
|
|
saveables = []
|
|
num_shards = len(self.values)
|
|
for shard_id in range(num_shards):
|
|
saveables.append(
|
|
TPUEmbeddingShardedSaveable(
|
|
self.values[shard_id],
|
|
shard_id,
|
|
num_shards,
|
|
self.shard_dim,
|
|
name,
|
|
)
|
|
)
|
|
return saveables
|
|
|
|
return {base.VARIABLE_VALUE_KEY: _saveable_factory}
|
|
|
|
def _dense_var_to_tensor(self, dtype=None, name=None, as_ref=False):
|
|
"""Converts a variable to a tensor."""
|
|
# pylint: disable=protected-access
|
|
if tpu_util.enclosing_tpu_context() is None:
|
|
return self._values[0].read_value()
|
|
else:
|
|
return self._read_variable_op()
|
|
|
|
def read_value(self) -> Any:
|
|
if tpu_util.enclosing_tpu_context() is None:
|
|
raise NotImplementedError(
|
|
"Reading in cross replica mode is not yet supported"
|
|
"for TPUEmbeddingShardedVariable."
|
|
)
|
|
else:
|
|
return self._read_variable_op()
|
|
|
|
def assign(
|
|
self,
|
|
value: Any,
|
|
use_locking: bool = False,
|
|
name: Optional[Any] = None,
|
|
read_value: bool = True,
|
|
) -> Any:
|
|
if tpu_util.enclosing_tpu_context() is None:
|
|
# Running in a host context
|
|
for device in self.distribute_strategy.extended.worker_devices:
|
|
with ops.device(device):
|
|
self.assign_on_device(device, value)
|
|
return tpu_util.make_raw_assign_fn(
|
|
gen_resource_variable_ops.assign_variable_op
|
|
)(
|
|
self,
|
|
value=value,
|
|
use_locking=use_locking,
|
|
name=name,
|
|
read_value=read_value,
|
|
)
|
|
|
|
def assign_on_device(self, device, value):
|
|
if self._packed_var is None:
|
|
raise NotImplementedError("Required packed variable support")
|
|
with ops.device(device):
|
|
gen_resource_variable_ops.assign_variable_op(
|
|
resource=self._packed_var.handle, value=value
|
|
)
|
|
|
|
def read_from_device(self, device):
|
|
if self._packed_var is None:
|
|
raise NotImplementedError("Required packed variable support")
|
|
with ops.device(device):
|
|
return gen_resource_variable_ops.read_variable_op(
|
|
resource=self._packed_var.handle, dtype=self.dtype
|
|
)
|
|
|
|
|
|
# TODO(pineapplejuice233): Add debug string representation of the class.
|
|
PartitionedCsrFormatTensor = collections.namedtuple(
|
|
"PartitionedCsrFormatTensor",
|
|
[
|
|
"row_pointers",
|
|
"sorted_sample_ids",
|
|
"sorted_token_ids",
|
|
"sorted_gains",
|
|
"sample_count",
|
|
"num_minibatches_per_physical_sparse_core",
|
|
],
|
|
)
|
|
|
|
|
|
def _clone_feature_config(feature_config):
|
|
old_to_new_table = {}
|
|
new_features = []
|
|
|
|
for old_feature in nest.flatten(feature_config):
|
|
feature = copy.copy(old_feature)
|
|
if feature.table not in old_to_new_table:
|
|
old_to_new_table[feature.table] = copy.copy(feature.table)
|
|
feature.table = old_to_new_table[feature.table]
|
|
new_features.append(feature)
|
|
|
|
return nest.pack_sequence_as(feature_config, new_features)
|
|
|
|
|
|
def _stack_tables_with_same_table_dim_and_optimizer(
|
|
table_config: Sequence[TableConfig],
|
|
flat_features: Sequence[Tuple[Any, FeatureConfig]],
|
|
num_partitions: int,
|
|
num_sc_per_partition: int,
|
|
sparse_core_embedding_config: Optional[SparseCoreEmbeddingConfig] = None,
|
|
) -> TableStacking:
|
|
"""Stack tables with the same table dim and optimizer."""
|
|
logging.info("Number of tables before stacking is %d", len(table_config))
|
|
disable_table_stacking = False
|
|
if sparse_core_embedding_config:
|
|
disable_table_stacking = sparse_core_embedding_config.disable_table_stacking
|
|
|
|
if disable_table_stacking:
|
|
logging.warn("Table stacking is disabled.")
|
|
|
|
stacker = _pywrap_sparse_core_layout.SparseCoreLayoutStacker(
|
|
num_partitions=num_partitions,
|
|
sparse_cores_per_partition=num_sc_per_partition,
|
|
disable_table_stacking=disable_table_stacking,
|
|
)
|
|
s = TableStacking()
|
|
s.table_name_to_table = {table.name: table for table in table_config}
|
|
table_to_num_samples = {table.name: 0 for table in table_config}
|
|
table_to_num_features = {table.name: 0 for table in table_config}
|
|
for _, feature in flat_features:
|
|
table_to_num_samples[feature.table.name] += functools.reduce(
|
|
operator.mul, feature.output_shape
|
|
)
|
|
table_to_num_features[feature.table.name] += 1
|
|
|
|
# First generate stacking for any tables our caller didn't stack for us.
|
|
# Note that we process the tables sorted by name so the ordering is
|
|
# deterministic.
|
|
sorted_tables = sorted(table_config, key=lambda t: t.name)
|
|
for table in sorted_tables:
|
|
if not table.layout:
|
|
# All tables in a stack have to have the same hyperparemeters; this key
|
|
# contains everything we care about. The key is an arbitrary string
|
|
# whose value is not particularly meaningful except that it has to be
|
|
# different if the tables cannot be stacked together.
|
|
#
|
|
# Note that later we rewrite the stack name based on the tables in that
|
|
# stack; this is just a temporary initial name.
|
|
#
|
|
# The key does not need to include the embedding width; that is handled
|
|
# separately.
|
|
key_tuple = (
|
|
# Optimizers don't have a repr but do support hash.
|
|
hash(table.optimizer),
|
|
# Quantization configs don't have a hash but do support repr.
|
|
repr(table.quantization_config),
|
|
)
|
|
key_str = hashlib.sha1(
|
|
repr(key_tuple).encode(),
|
|
usedforsecurity=False,
|
|
).hexdigest()
|
|
key = "_xxtpuv3internal_" + key_str
|
|
stacker.AddTable(
|
|
table_name=table.name,
|
|
table_height=table.vocabulary_size,
|
|
table_width=table.dim,
|
|
group=key,
|
|
output_samples=table_to_num_samples[table.name],
|
|
num_features=table_to_num_features[table.name],
|
|
)
|
|
# First generate stacking for any tables our caller didn't stack for us.
|
|
# Note that we process the tables sorted by name so the ordering is
|
|
# deterministic.
|
|
# Put the layout information we just computed back into the tables, so we
|
|
# can treat tables whose layouts were given by the caller and tables whose
|
|
# layouts we computed the same.
|
|
for layout in stacker.GetLayouts().tables:
|
|
table = s.table_name_to_table[layout.table_name]
|
|
assert not table.layout # It's a bug if it was already set.
|
|
table.layout = layout
|
|
|
|
# Collect all the layout information from all the tables, whether we just
|
|
# computed it above, or whether the caller passed it as part of the
|
|
# TableConfig:
|
|
tables_by_stack = collections.defaultdict(list)
|
|
for table in sorted_tables:
|
|
layout = table.layout
|
|
assert layout.table_name == table.name
|
|
s.table_to_layout[table.name] = layout
|
|
tables_by_stack[layout.stacked_table_name].append(table)
|
|
|
|
for stack_name, tables in tables_by_stack.items():
|
|
s.quantization_configs[stack_name] = tables[0].quantization_config
|
|
s.stacked_table_to_tables[stack_name] = tables
|
|
|
|
logging.vlog(1, "Stacked table name: %s", stack_name)
|
|
for table in tables:
|
|
layout = table.layout
|
|
logging.vlog(
|
|
1,
|
|
" Table %s: offset %d, rotation %d",
|
|
table.name,
|
|
layout.sparse_core_shard_row_offset,
|
|
layout.sparse_core_shard_rotation,
|
|
)
|
|
s.table_to_stacked_table_offset[table.name] = (
|
|
stack_name,
|
|
layout.sparse_core_shard_row_offset
|
|
* num_partitions
|
|
* num_sc_per_partition,
|
|
layout.sparse_core_shard_rotation,
|
|
)
|
|
# Update dimensions in the table to the padded dimensions.
|
|
table.vocabulary_size = layout.unsharded_padded_shape[0]
|
|
table.dim = layout.unsharded_padded_shape[1]
|
|
s.table_to_padding_rows[table.name] = (
|
|
layout.unsharded_padded_shape[0] - layout.unsharded_shape[0]
|
|
)
|
|
s.table_to_padding_columns[table.name] = (
|
|
layout.unsharded_padded_shape[1] - layout.unsharded_shape[1]
|
|
)
|
|
|
|
logging.info(
|
|
"Number of tables after stacking is %d.",
|
|
len(s.stacked_table_to_tables),
|
|
)
|
|
|
|
s.table_to_sample_count = {
|
|
table_name: 0 for table_name in s.stacked_table_to_tables
|
|
}
|
|
for feature_path, feature in flat_features:
|
|
stacked_table_name = s.table_to_stacked_table_offset[feature.table.name][
|
|
0
|
|
]
|
|
s.feature_to_sample_offset[feature_path] = s.table_to_sample_count[
|
|
stacked_table_name
|
|
]
|
|
s.table_to_sample_count[stacked_table_name] += functools.reduce(
|
|
operator.mul, feature.output_shape
|
|
)
|
|
|
|
return s
|
|
|
|
|
|
# TODO(b/233952762): Add tests of this version of the mid-level API.
|
|
@tf_export("tpu.experimental.embedding.TPUEmbeddingV2")
|
|
class TPUEmbeddingV2(tpu_embedding_base.TPUEmbeddingBase):
|
|
"""The TPUEmbedding mid level API running on TPU with sparse core accelerator."""
|
|
|
|
DEFAULT_MAX_IDS_PER_TABLE = 256
|
|
DEFAULT_MAX_UNIQUE_IDS_PER_TABLE = 256
|
|
|
|
def __init__(
|
|
self,
|
|
feature_config: Union[tpu_embedding_v2_utils.FeatureConfig, Iterable], # pylint:disable=g-bare-generic
|
|
optimizer: Optional[tpu_embedding_v2_utils._Optimizer] = None, # pylint:disable=protected-access
|
|
pipeline_execution_with_tensor_core: bool = False,
|
|
sparse_core_embedding_config: Optional[SparseCoreEmbeddingConfig] = None,
|
|
):
|
|
"""Creates the TPUEmbeddingV2 mid level API object.
|
|
|
|
Args:
|
|
feature_config: A nested structure of
|
|
`tf.tpu.experimental.embedding.FeatureConfig` configs.
|
|
optimizer: An instance of one of `tf.tpu.experimental.embedding.SGD`,
|
|
`tf.tpu.experimental.embedding.Adagrad` or
|
|
`tf.tpu.experimental.embedding.Adam`. When not created under TPUStrategy
|
|
may be set to None to avoid the creation of the optimizer slot
|
|
variables, useful for optimizing memory consumption when exporting the
|
|
model for serving where slot variables aren't needed.
|
|
pipeline_execution_with_tensor_core: If True, the TPU embedding
|
|
computations will overlap with the TensorCore computations (and hence
|
|
will be one step old). Set to True for improved performance.
|
|
sparse_core_embedding_config: Configs for sparse core embedding including
|
|
settings for table stacking, input feature static buffer size etc.
|
|
|
|
Raises:
|
|
ValueError: If optimizer is not one of tf.tpu.experimental.embedding.(SGD,
|
|
Adam or Adagrad) or None when created under a TPUStrategy.
|
|
RuntimeError: If not created under TPUStrategy.
|
|
"""
|
|
# We do a clone on the feature_config here as we will alter settings in it
|
|
# and we don't want the user to see these. We can't just use clone here
|
|
# as we need to maintain some object relationships.
|
|
super().__init__(_clone_feature_config(feature_config), optimizer)
|
|
self._strategy = distribute_lib.get_strategy()
|
|
if not isinstance(
|
|
self._strategy, (tpu_strategy.TPUStrategy, tpu_strategy.TPUStrategyV2)
|
|
):
|
|
raise RuntimeError(
|
|
"TPUEmbeddingV2 should be created under TPUStrategy but found {}."
|
|
.format(self._strategy)
|
|
)
|
|
|
|
# TODO(pineapplejuice233): Remove this once weight decay is supported.
|
|
for table in self._table_config:
|
|
if (
|
|
table.optimizer.weight_decay_factor is not None
|
|
or table.optimizer.multiply_weight_decay_factor_by_learning_rate
|
|
is not None
|
|
):
|
|
raise NotImplementedError(
|
|
"weight_decay_factor and"
|
|
" multiply_weight_decay_factor_by_learning_rate are not supported"
|
|
f" yet. But found in table {table.name} setting."
|
|
)
|
|
|
|
self._num_sc_per_chip = (
|
|
self._strategy.extended.tpu_hardware_feature.num_embedding_devices_per_chip
|
|
)
|
|
if self._num_sc_per_chip == 0:
|
|
logging.warning(
|
|
"No embedding devices per chip info is found. Using 4 as the default"
|
|
" value for SparseCore."
|
|
)
|
|
self._num_sc_per_chip = 4
|
|
|
|
self._num_sc_shards = (
|
|
self._strategy.num_replicas_in_sync * self._num_sc_per_chip
|
|
)
|
|
|
|
# We need this in multiple places, so avoid flattening multiple times.
|
|
# This order will also be used when stacking features.
|
|
self._flat_features = nest.flatten_with_joined_string_paths(
|
|
self._feature_config
|
|
)
|
|
|
|
if sparse_core_embedding_config is None:
|
|
self._sparse_core_embedding_config = SparseCoreEmbeddingConfig()
|
|
logging.warning(
|
|
"SparseCoreEmbeddingConfig is not provided. Using default values %s",
|
|
self._sparse_core_embedding_config,
|
|
)
|
|
else:
|
|
self._sparse_core_embedding_config = sparse_core_embedding_config
|
|
|
|
self._s = _stack_tables_with_same_table_dim_and_optimizer(
|
|
self._table_config,
|
|
self._flat_features,
|
|
self._strategy.num_replicas_in_sync,
|
|
self._num_sc_per_chip,
|
|
self._sparse_core_embedding_config,
|
|
)
|
|
|
|
self._table_name_to_table = self._s.table_name_to_table
|
|
self._stacked_table_to_tables = self._s.stacked_table_to_tables
|
|
self._table_to_padding_columns = self._s.table_to_padding_columns
|
|
self._table_to_padding_rows = self._s.table_to_padding_rows
|
|
self._table_to_stacked_table_offset = self._s.table_to_stacked_table_offset
|
|
self._table_to_sample_count = self._s.table_to_sample_count
|
|
self._feature_to_sample_offset = self._s.feature_to_sample_offset
|
|
self._quantization_configs = self._s.quantization_configs
|
|
|
|
# These hyperparameters will be provided by the FDO. Currently hardcode
|
|
# here just for testing.
|
|
self.max_ids_per_chip_per_sample = (
|
|
self._sparse_core_embedding_config.max_ids_per_chip_per_sample
|
|
)
|
|
self.max_minibatches_per_sc = 64
|
|
|
|
self._table_to_max_ids_per_sparse_core = {}
|
|
self._table_to_max_unique_ids_per_sparse_core = {}
|
|
|
|
self._update_sparse_core_buffer_size_after_table_stacking()
|
|
|
|
self._pipelining = pipeline_execution_with_tensor_core
|
|
self._initializers_shard_info_by_default = True
|
|
|
|
def _compute_sc_shard_info(
|
|
self,
|
|
table: TableConfig,
|
|
partition_shape: tuple[int, int],
|
|
partition_offset: List[int],
|
|
total_vocab_size: int,
|
|
sc_idx: int,
|
|
) -> base.ShardInfo:
|
|
# Scale the partition to get sizes for the current table,
|
|
# then select this sc shard.
|
|
sc_shard_size = (
|
|
table.vocabulary_size
|
|
* partition_shape[0]
|
|
// total_vocab_size
|
|
// self._num_sc_per_chip
|
|
)
|
|
sc_shard_offset = (
|
|
table.vocabulary_size
|
|
* partition_offset[0]
|
|
// total_vocab_size
|
|
) + sc_idx * sc_shard_size
|
|
|
|
return base.ShardInfo([sc_shard_size, table.dim], [sc_shard_offset, 0])
|
|
|
|
def _compute_sc_shard_idx_and_offset(
|
|
self,
|
|
table_name: str,
|
|
shard_info: base.ShardInfo
|
|
) -> tuple[int, int]:
|
|
tpu_devices = self._strategy.extended._tpu_devices # pylint:disable=protected-access
|
|
num_replicas, num_cores_per_replica = tpu_devices.shape
|
|
num_devices = num_replicas * num_cores_per_replica
|
|
|
|
shift = self._s.table_to_stacked_table_offset[table_name][2]
|
|
shard_index = shard_info.offset[0] // shard_info.shape[0]
|
|
# Rotate the shards.
|
|
shard_index = (shard_index - shift) % self._num_sc_shards
|
|
num_sc = num_devices * self._num_sc_per_chip
|
|
|
|
return shard_index, num_sc
|
|
|
|
def _update_sparse_core_buffer_size_after_table_stacking(self):
|
|
"""Update the sparse core buffer size after table stacking."""
|
|
for table_name in self._stacked_table_to_tables:
|
|
if (
|
|
self._sparse_core_embedding_config.max_ids_per_table is None
|
|
or table_name
|
|
not in self._sparse_core_embedding_config.max_ids_per_table
|
|
):
|
|
logging.warning(
|
|
"Table %s is not found in max_ids_per_table provided by"
|
|
" SparseCoreEmbeddingConfig. Using default value 256.",
|
|
table_name,
|
|
)
|
|
self._table_to_max_ids_per_sparse_core[table_name] = (
|
|
self.DEFAULT_MAX_IDS_PER_TABLE
|
|
)
|
|
else:
|
|
self._table_to_max_ids_per_sparse_core[table_name] = (
|
|
self._sparse_core_embedding_config.max_ids_per_table[table_name]
|
|
)
|
|
if (
|
|
self._sparse_core_embedding_config.max_unique_ids_per_table is None
|
|
or table_name
|
|
not in self._sparse_core_embedding_config.max_unique_ids_per_table
|
|
):
|
|
logging.warning(
|
|
(
|
|
"Table %s is not found in max_unique_ids_per_table provided by"
|
|
" SparseCoreEmbeddingConfig. Using default value 256."
|
|
),
|
|
table_name,
|
|
)
|
|
self._table_to_max_unique_ids_per_sparse_core[table_name] = (
|
|
self.DEFAULT_MAX_UNIQUE_IDS_PER_TABLE
|
|
)
|
|
else:
|
|
self._table_to_max_unique_ids_per_sparse_core[table_name] = (
|
|
self._sparse_core_embedding_config.max_unique_ids_per_table[
|
|
table_name
|
|
]
|
|
)
|
|
|
|
@property
|
|
def embedding_tables(
|
|
self,
|
|
) -> Dict[str, tf_variables.Variable]:
|
|
"""Returns a dict of embedding tables, keyed by stacked table name."""
|
|
self._maybe_build()
|
|
# Only return the tables and not the slot variables.
|
|
return {
|
|
stacked_table_name: self._variables[stacked_table_name]["parameters"]
|
|
for stacked_table_name in self._stacked_table_to_tables
|
|
}
|
|
|
|
@property
|
|
def embedding_table_shards(
|
|
self,
|
|
) -> Dict[tpu_embedding_v2_utils.TableConfig, List[tf_variables.Variable]]:
|
|
"""Returns a dict of embedding tables, keyed by `TableConfig`."""
|
|
self._maybe_build()
|
|
|
|
# This reflects the device assignment used by the TPU Strategy.
|
|
ordered_devices = []
|
|
for devices in self._strategy.extended._tpu_devices: # pylint: disable=protected-access
|
|
ordered_devices.extend(devices)
|
|
|
|
table_shards = {
|
|
name: [
|
|
(device, var.read_from_device(device)) for device in ordered_devices
|
|
]
|
|
for name, var in self.embedding_tables.items()
|
|
}
|
|
|
|
return table_shards
|
|
|
|
@property
|
|
def embedding_layouts(
|
|
self,
|
|
) -> Dict[str, sparse_core_layout_pb2.SparseCoreTableLayout]:
|
|
"""Returns how the tables are laid out in the variables.
|
|
|
|
The SparseCoreTableLayout describes how a table is stored in its internal
|
|
state. You need this only if you need to pull apart the internal state.
|
|
"""
|
|
return self._s.table_to_layout
|
|
|
|
@property
|
|
def variables(
|
|
self,
|
|
) -> Dict[
|
|
tpu_embedding_v2_utils.TableConfig, Dict[str, tf_variables.Variable]
|
|
]:
|
|
"""Returns a dict of variables, keyed by `TableConfig`, then by slot name."""
|
|
self._maybe_build()
|
|
return self._variables
|
|
|
|
def _variable_creator(
|
|
self,
|
|
next_creator: Callable[..., tf_variables.Variable],
|
|
**kwargs,
|
|
) -> Callable[..., Any]:
|
|
return make_sharded_variable_creator(self._strategy)(next_creator, **kwargs)
|
|
|
|
def _wrap_initializer(self, initializer):
|
|
"""Wraps the initializer to ensure that it is sharding aware."""
|
|
arg_spec = tf_inspect.getfullargspec(initializer)
|
|
sharding_aware = (
|
|
"shard_info" in arg_spec.args or "shard_info" in arg_spec.kwonlyargs
|
|
)
|
|
if sharding_aware:
|
|
return initializer
|
|
|
|
def wrapper(shape, dtype, shard_info: base.ShardInfo):
|
|
del shape
|
|
return initializer(shape=shard_info.shape, dtype=dtype)
|
|
|
|
return wrapper
|
|
|
|
def _get_shard_info_for_table(
|
|
self,
|
|
table: TableConfig,
|
|
device_idx: int,
|
|
num_devices: int,
|
|
) -> base.ShardInfo:
|
|
"""Returns the shard info for the given table."""
|
|
device_shard_len = table.vocabulary_size // num_devices
|
|
sc_shard_len = device_shard_len // self._num_sc_per_chip
|
|
shift = self._s.table_to_stacked_table_offset[table.name][2]
|
|
shift_rows = shift * sc_shard_len
|
|
row_offset = (
|
|
device_idx * device_shard_len - shift_rows
|
|
) % table.vocabulary_size
|
|
return base.ShardInfo(
|
|
(device_shard_len, table.dim),
|
|
(row_offset, 0),
|
|
)
|
|
|
|
def _initialize_stacked_table_for_device(
|
|
self,
|
|
stacked_tables: List[TableConfig],
|
|
device_idx: int,
|
|
num_devices: int,
|
|
) -> dict[str, tensor.Tensor]:
|
|
"""Initializes the stacked tables and slots shards for a single device."""
|
|
table_dim = stacked_tables[0].dim
|
|
variable_dtype = stacked_tables[0].dtype
|
|
slot_shards = {}
|
|
replicated_slot_shards = {}
|
|
for table in stacked_tables:
|
|
if table.optimizer is not None:
|
|
initializers = {
|
|
slot: initializer
|
|
for slot, initializer in zip(
|
|
table.optimizer._slot_names(), # pylint:disable=protected-access
|
|
table.optimizer._slot_initializers(), # pylint:disable=protected-access
|
|
)
|
|
}
|
|
else:
|
|
initializers = {}
|
|
|
|
initializers[_PARAMETER_NAME] = table.initializer
|
|
|
|
shard_info = self._get_shard_info_for_table(
|
|
table, device_idx, num_devices
|
|
)
|
|
for slot, initializer in initializers.items():
|
|
device_shard = self._wrap_initializer(initializer)(
|
|
shape=(table.vocabulary_size, table.dim),
|
|
dtype=variable_dtype,
|
|
shard_info=shard_info,
|
|
)
|
|
sc_shards = array_ops.reshape(
|
|
device_shard, (self._num_sc_per_chip, -1, table.dim)
|
|
)
|
|
slot_shards.setdefault(slot, []).append(sc_shards)
|
|
|
|
# Keep track of the replicated slot variable shards required by the custom
|
|
# combiner.
|
|
if isinstance(table.combiner, tpu_embedding_v2_utils.CustomCombiner):
|
|
combiner_weights_shape = (table.combiner.num_weights,)
|
|
replicated_slot_shards = {
|
|
slot: self._wrap_initializer(initializer)(
|
|
shape=combiner_weights_shape,
|
|
dtype=dtypes.float32,
|
|
shard_info=base.ShardInfo(
|
|
shape=combiner_weights_shape,
|
|
offset=device_idx,
|
|
),
|
|
)
|
|
for slot, initializer in zip(
|
|
table.combiner._slot_names(), # pylint:disable=protected-access
|
|
table.combiner._slot_initializers(), # pylint:disable=protected-access
|
|
strict=True, # number of names and initializers must match.
|
|
)
|
|
}
|
|
|
|
slots = {}
|
|
for slot, shards in slot_shards.items():
|
|
concated_shards = array_ops.concat(shards, axis=1)
|
|
concated_shards = array_ops.reshape(concated_shards, (-1, table_dim))
|
|
slots[slot] = concated_shards
|
|
slots.update(replicated_slot_shards)
|
|
return slots
|
|
|
|
def _initialize_stacked_table_all_devices(
|
|
self,
|
|
stacked_tables: List[TableConfig],
|
|
stacked_table_name: str,
|
|
) -> dict[str, tensor.Tensor]:
|
|
"""Initializes the stacked tables and slots shards for all devices."""
|
|
table_dim = stacked_tables[0].dim
|
|
|
|
tpu_devices = self._strategy.extended._tpu_devices # pylint:disable=protected-access
|
|
num_devices = tpu_devices.size
|
|
num_scs = num_devices * self._num_sc_per_chip
|
|
|
|
for table in stacked_tables:
|
|
if table.vocabulary_size % num_scs != 0:
|
|
raise ValueError(
|
|
"Only evenly sharding across devices is currently supported. "
|
|
f"Got vocab size {table.vocabulary_size} and {num_scs} sparse cores"
|
|
)
|
|
if table.dim != table_dim:
|
|
raise ValueError(
|
|
"Only tables with the same dimension are currently supported. "
|
|
f"Got table {table.name} with dimension {table.dim} in stacked "
|
|
f"table {stacked_table_name} with dimension {table_dim}"
|
|
)
|
|
|
|
parameters = {}
|
|
for idx, device in enumerate(tpu_devices.flatten()):
|
|
with ops.device(device):
|
|
device_parameters = self._initialize_stacked_table_for_device(
|
|
stacked_tables, idx, num_devices
|
|
)
|
|
for slot, shard in device_parameters.items():
|
|
parameters.setdefault(slot, []).append(shard)
|
|
return parameters
|
|
|
|
@def_function.function
|
|
def _batch_initialize_tables_fn(
|
|
self,
|
|
) -> Dict[str, Dict[str, List[tensor.Tensor]]]:
|
|
tensors = {}
|
|
for stacked_table_name, tables in self._s.stacked_table_to_tables.items():
|
|
tensors[stacked_table_name] = self._initialize_stacked_table_all_devices(
|
|
tables, stacked_table_name=stacked_table_name
|
|
)
|
|
return tensors
|
|
|
|
def _batch_initialize_tables(
|
|
self,
|
|
) -> Dict[str, Dict[str, List[tensor.Tensor]]]:
|
|
logging.info("Batch initializing embedding tables.")
|
|
tpu_devices = self._strategy.extended._tpu_devices # pylint:disable=protected-access
|
|
with ops.device(device_util.get_host_for_device(tpu_devices[0][0])):
|
|
return self._batch_initialize_tables_fn()
|
|
|
|
def _create_variables(
|
|
self,
|
|
stacked_tables: List[tpu_embedding_v2_utils.TableConfig],
|
|
stacked_table_name: str,
|
|
initialized_tensors: dict[str, dict[str, List[tensor.Tensor]]],
|
|
) -> Dict[str, tf_variables.Variable]:
|
|
"""Create all variables including table variables and slot variables."""
|
|
total_vocab_size = sum([table.vocabulary_size for table in stacked_tables])
|
|
table_dim = stacked_tables[0].dim
|
|
variable_shape = (total_vocab_size, table_dim)
|
|
variable_dtype = dtypes.float32
|
|
optimizer = stacked_tables[0].optimizer
|
|
|
|
def table_initialize_fn_non_batch(shape, dtype, shard_info=None):
|
|
# If enable fast table initialization, we will initialize the table
|
|
# directly on the device and use the initializer from the first table.
|
|
if self._sparse_core_embedding_config.enable_fast_table_initialization:
|
|
return stacked_tables[0].initializer(
|
|
shape=(shard_info.shape[0], stacked_tables[0].dim),
|
|
dtype=dtype,
|
|
)
|
|
|
|
# Concat all the tables along the first axis.
|
|
concat_tensors = []
|
|
|
|
# Temporary patch, we need to initialize tables with the SC level
|
|
# sharding. Note that we need to ensure that the vocab size is divisible
|
|
# by the global number of SC.
|
|
for i in range(self._num_sc_per_chip):
|
|
# Each underlying table has column lookups rotated by 1 to avoid hot
|
|
# spots on core 0 for id=0. We shift the initializer as well to help
|
|
# with comparisons against CPU.
|
|
for table in stacked_tables:
|
|
arg_spec = tf_inspect.getfullargspec(table.initializer)
|
|
sharding_aware = (
|
|
"shard_info" in arg_spec.args
|
|
or "shard_info" in arg_spec.kwonlyargs
|
|
)
|
|
|
|
if shard_info:
|
|
sc_shard_info = self._compute_sc_shard_info(
|
|
table,
|
|
shard_info.shape,
|
|
shard_info.offset,
|
|
total_vocab_size,
|
|
i,
|
|
)
|
|
if not sharding_aware:
|
|
shard_index, shard_offset = self._compute_sc_shard_idx_and_offset(
|
|
table.name, sc_shard_info
|
|
)
|
|
sc_shard = table.initializer(
|
|
shape=(table.vocabulary_size, table.dim), dtype=dtype
|
|
)[shard_index::shard_offset, :]
|
|
else:
|
|
sc_shard = table.initializer(
|
|
shape=(table.vocabulary_size, table.dim),
|
|
dtype=dtype,
|
|
shard_info=sc_shard_info,
|
|
)
|
|
else:
|
|
sc_shard = table.initializer(
|
|
shape=(
|
|
(table.vocabulary_size * shape[0])
|
|
// total_vocab_size
|
|
// self._num_sc_per_chip,
|
|
table.dim,
|
|
),
|
|
dtype=dtype,
|
|
)
|
|
concat_tensors.append(sc_shard)
|
|
return array_ops.concat(concat_tensors, axis=0)
|
|
|
|
def batch_initialize_fn(name, shard_info, replicated=False):
|
|
if not initialized_tensors:
|
|
initialized_tensors.update(self._batch_initialize_tables())
|
|
if not replicated:
|
|
# This is the path for the table and optimizer slot variables (which
|
|
# currently have the same shape as the table).
|
|
shard_id = shard_info.offset[0] // shard_info.shape[0]
|
|
else:
|
|
# This is the path for the combiner slot variable (1D and is of size
|
|
# combiner.num_weights). The device ID on which this shard is
|
|
# initialized is passed via the first element of shard_info.offset.
|
|
shard_id = shard_info.offset[0]
|
|
res = initialized_tensors[stacked_table_name][name][shard_id]
|
|
assert shard_info.shape == res.shape
|
|
return res
|
|
|
|
def table_initialize_fn(shape, dtype, shard_info=None):
|
|
if not shard_info or not self._initializers_shard_info_by_default:
|
|
# This is the legacy loading path.
|
|
return table_initialize_fn_non_batch(shape, dtype, shard_info)
|
|
return batch_initialize_fn(_PARAMETER_NAME, shard_info)
|
|
|
|
def slot_initialize_fn(
|
|
name, shape, dtype, initializer, shard_info=None, replicated=False
|
|
):
|
|
if not shard_info or not self._initializers_shard_info_by_default:
|
|
# This is the legacy loading path.
|
|
return initializer(shape, dtype)
|
|
return batch_initialize_fn(name, shard_info, replicated)
|
|
|
|
def getter(name, shape, dtype, initializer, trainable):
|
|
initial_value = functools.partial(initializer, shape=shape, dtype=dtype)
|
|
# _add_variable_with_custom_getter clears the shape sometimes, so we
|
|
# take the global shape from outside the getter.
|
|
return tf_variables.Variable(
|
|
name=name,
|
|
initial_value=initial_value,
|
|
shape=shape,
|
|
dtype=dtype,
|
|
trainable=trainable,
|
|
)
|
|
|
|
def variable_creator(name, initializer, shape, dtype):
|
|
# Use add_variable_with_custom_getter here so that we take advantage of
|
|
# the checkpoint loading to allow restore before the variables get
|
|
# created which avoids double initialization.
|
|
return self._add_variable_with_custom_getter(
|
|
name=name,
|
|
initializer=initializer,
|
|
shape=shape,
|
|
dtype=dtype,
|
|
getter=getter,
|
|
trainable=False,
|
|
)
|
|
|
|
with variable_scope.variable_creator_scope(self._variable_creator):
|
|
parameters = variable_creator(
|
|
stacked_table_name,
|
|
table_initialize_fn,
|
|
variable_shape,
|
|
variable_dtype,
|
|
)
|
|
|
|
def slot_creator(name, initializer, shape, dtype):
|
|
return variable_creator(
|
|
stacked_table_name + "/" + name, initializer, shape, dtype
|
|
)
|
|
|
|
slot_vars = {}
|
|
if optimizer is not None:
|
|
with variable_scope.variable_creator_scope(self._variable_creator):
|
|
slot_vars = optimizer._create_slots( # pylint: disable=protected-access
|
|
parameters,
|
|
variable_creator=functools.partial(
|
|
slot_creator, shape=variable_shape, dtype=variable_dtype
|
|
),
|
|
initializer_wrapper=lambda slot, initializer: functools.partial(
|
|
slot_initialize_fn,
|
|
name=slot,
|
|
initializer=initializer,
|
|
replicated=False,
|
|
),
|
|
)
|
|
|
|
slot_vars[_PARAMETER_NAME] = parameters
|
|
return slot_vars
|
|
|
|
def _create_variables_and_slots(
|
|
self,
|
|
) -> Dict[str, Dict[str, tf_variables.Variable]]:
|
|
"""Create variables for TPU embeddings.
|
|
|
|
Returns:
|
|
A dict of dicts. The outer dict is keyed by the table names and the inner
|
|
dicts are keyed by 'parameters' and the slot variable names.
|
|
"""
|
|
variables = {}
|
|
# Batch initialized table and slot variables. It will be lazily initialized
|
|
# when it is first used inside the variable initializers.
|
|
table_tensors = {}
|
|
for stacked_table_name, tables in self._s.stacked_table_to_tables.items():
|
|
variables[stacked_table_name] = self._create_variables(
|
|
tables,
|
|
stacked_table_name=stacked_table_name,
|
|
initialized_tensors=table_tensors,
|
|
)
|
|
return variables
|
|
|
|
def _track_restore_info_for_cpu(self) -> None:
|
|
layouts = sparse_core_layout_pb2.SparseCoreTableLayouts()
|
|
layouts.tables.extend(self.embedding_layouts.values())
|
|
logging.info(
|
|
"Adding sparse core layouts for %s tables", len(layouts.tables)
|
|
)
|
|
with ops.device("/cpu:0"):
|
|
self._track_trackable(
|
|
tpu_embedding_v3_utils.SparseCoreLayoutsTrackable(
|
|
constant_op.constant(
|
|
layouts.SerializeToString(), dtype=dtypes.string
|
|
)
|
|
),
|
|
tpu_embedding_v3_utils.SPARSECORE_LAYOUTS_CHECKPOINT_KEY,
|
|
)
|
|
|
|
def _checkpoint_adapter(self, path):
|
|
# The TPUEmbedding may need to reshard checkpoint values during restore.
|
|
return tpu_embedding_v3_checkpoint_adapter.TpuEmbeddingV3CheckpointAdapter.create_from_checkpoint(
|
|
path
|
|
)
|
|
|
|
def _maybe_build(self):
|
|
if not self._built:
|
|
# This can be called while tracing a function, so we wrap the
|
|
# initialization code with init_scope so it runs eagerly, this means that
|
|
# it will not be included in the function graph generated by tracing so
|
|
# that we can be sure that we only initialize the TPU for embeddings
|
|
# exactly once.
|
|
with ops.init_scope():
|
|
self.build()
|
|
|
|
def build(self):
|
|
"""Create variables and slots variables for TPU embeddings."""
|
|
if self._built:
|
|
return
|
|
self._variables = self._create_variables_and_slots()
|
|
self._track_restore_info_for_cpu()
|
|
self._built = True
|
|
logging.info("TPUEmbedding built.")
|
|
|
|
def apply_gradients(
|
|
self,
|
|
gradients: Any,
|
|
preserved_outputs: Dict[str, PartitionedCsrFormatTensor],
|
|
):
|
|
"""Applies the gradient update to the embedding tables.
|
|
|
|
If a gradient of `None` is passed in any position of the nested structure,
|
|
then a gradient update with a zero gradient is applied for that feature.
|
|
For optimizers like SGD or Adagrad, this is the same as applying no update
|
|
at all. For lazy Adam and other sparsely applied optimizers with decay,
|
|
ensure you understand the effect of applying a zero gradient.
|
|
|
|
Args:
|
|
gradients: A nested structure of gradients, with structure matching the
|
|
`feature_config` passed to this object.
|
|
preserved_outputs: A dicts of PartitionedCsrFormatTensor, coming from the
|
|
second output of the embedding lookup call.
|
|
|
|
Raises:
|
|
RuntimeError: if not built.
|
|
ValueError: If a non-`tf.Tensor` non-`None` gradient is passed in, or a
|
|
`tf.Tensor` of the incorrect shape is passed in. Also if
|
|
the size of any sequence in `gradients` does not match corresponding
|
|
sequence in `feature_config`.
|
|
TypeError: If the type of any sequence in `gradients` does not match
|
|
corresponding sequence in `feature_config`.
|
|
"""
|
|
if not self._built:
|
|
raise RuntimeError(
|
|
"apply_gradients called on unbuilt TPUEmbeddingV2 object. Please"
|
|
" either call the embedding lookup method first or manually call the"
|
|
" build method."
|
|
)
|
|
nest.assert_same_structure(self._feature_config, gradients)
|
|
|
|
# Note that stacking gradients is placed on the core of the trianing step
|
|
# to reduce the number of input/output arguments of the training loop during
|
|
# pipelining.
|
|
gradients = self._stack_gradients(gradients)
|
|
|
|
context = EmbeddingPipeliningContext(
|
|
_PIPELINE_MODE_BACKWARD, self._pipelining
|
|
)
|
|
context.Enter()
|
|
|
|
def _wrap_param(param, dtype=dtypes.float32):
|
|
if callable(param):
|
|
param = math_ops.cast(param(), dtype=dtype)
|
|
return ops.convert_to_tensor(param, dtype=dtype)
|
|
|
|
# Take num_minibatches_per_physical_sparse_core from any table as
|
|
# they are the same across tables.
|
|
num_minibatches_per_physical_sparse_core = list(preserved_outputs.values())[
|
|
0
|
|
].num_minibatches_per_physical_sparse_core
|
|
|
|
for table_name in self._stacked_table_to_tables:
|
|
gradient = gradients[table_name]
|
|
partitioned_tensor = preserved_outputs[table_name]
|
|
|
|
table = self.variables[table_name]["parameters"]
|
|
optimizer = self._stacked_table_to_tables[table_name][0].optimizer
|
|
if isinstance(optimizer, tpu_embedding_v2_utils.SGD):
|
|
updated_embedding_table = xla_ops.xla_sparse_dense_matmul_grad_with_sgd_and_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
activation_gradients=gradient,
|
|
learning_rate=_wrap_param(optimizer.learning_rate),
|
|
embedding_table=table.read_value(),
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
table.assign(updated_embedding_table)
|
|
elif isinstance(optimizer, tpu_embedding_v2_utils.Adagrad):
|
|
accumulators = self.variables[table_name]["accumulators"]
|
|
updated_embedding_table, updated_accumulator = (
|
|
xla_ops.xla_sparse_dense_matmul_grad_with_adagrad_and_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
activation_gradients=gradient,
|
|
learning_rate=_wrap_param(optimizer.learning_rate),
|
|
embedding_table=table.read_value(),
|
|
accumulator=accumulators.read_value(),
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
)
|
|
accumulators.assign(updated_accumulator)
|
|
table.assign(updated_embedding_table)
|
|
elif isinstance(optimizer, tpu_embedding_v2_utils.AdagradMomentum):
|
|
accumulators = self.variables[table_name]["accumulators"]
|
|
momenta = self.variables[table_name]["momenta"]
|
|
updated_embedding_table, updated_accumulator, updated_momenta = (
|
|
xla_ops.xla_sparse_dense_matmul_grad_with_adagrad_momentum_and_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
activation_gradients=gradient,
|
|
learning_rate=_wrap_param(optimizer.learning_rate),
|
|
embedding_table=table.read_value(),
|
|
accumulator=accumulators.read_value(),
|
|
momenta=momenta.read_value(),
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
use_nesterov=optimizer.use_nesterov,
|
|
exponent=optimizer.exponent,
|
|
beta1=optimizer.momentum,
|
|
beta2=optimizer.beta2,
|
|
epsilon=optimizer.epsilon,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
)
|
|
momenta.assign(updated_momenta)
|
|
accumulators.assign(updated_accumulator)
|
|
table.assign(updated_embedding_table)
|
|
elif isinstance(optimizer, tpu_embedding_v2_utils.Adam):
|
|
momenta = self.variables[table_name]["momenta"]
|
|
velocity = self.variables[table_name]["velocities"]
|
|
updated_embedding_table, updated_momenta, updated_velocity = (
|
|
xla_ops.xla_sparse_dense_matmul_grad_with_adam_and_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
activation_gradients=gradient,
|
|
learning_rate=_wrap_param(optimizer.learning_rate),
|
|
embedding_table=table.read_value(),
|
|
momenta=momenta.read_value(),
|
|
velocity=velocity.read_value(),
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
use_sum_inside_sqrt=optimizer.sum_inside_sqrt,
|
|
beta1=optimizer.beta_1,
|
|
beta2=optimizer.beta_2,
|
|
epsilon=optimizer.epsilon,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
)
|
|
velocity.assign(updated_velocity)
|
|
momenta.assign(updated_momenta)
|
|
table.assign(updated_embedding_table)
|
|
elif isinstance(optimizer, tpu_embedding_v2_utils.FTRL):
|
|
accumulators = self.variables[table_name]["accumulators"]
|
|
linears = self.variables[table_name]["linears"]
|
|
(updated_table_tensor, updated_accum_tensor, updated_linear_tensor) = (
|
|
xla_ops.xla_sparse_dense_matmul_grad_with_ftrl_and_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
activation_gradients=gradient,
|
|
learning_rate=_wrap_param(optimizer.learning_rate),
|
|
embedding_table=table.read_value(),
|
|
accumulator=accumulators.read_value(),
|
|
linear=linears.read_value(),
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
multiply_linear_by_learning_rate=optimizer.multiply_linear_by_learning_rate,
|
|
beta=optimizer.beta,
|
|
learning_rate_power=optimizer.learning_rate_power,
|
|
l1_regularization_strength=optimizer.l1_regularization_strength,
|
|
l2_regularization_strength=optimizer.l2_regularization_strength,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
)
|
|
linears.assign(updated_linear_tensor)
|
|
accumulators.assign(updated_accum_tensor)
|
|
table.assign(updated_table_tensor)
|
|
else:
|
|
raise ValueError("Unsupported optimizer in minibatching mode.")
|
|
|
|
context.Exit()
|
|
|
|
def __call__(
|
|
self, features: Any, weights: Optional[Any] = None
|
|
) -> Tuple[Any, Dict[str, PartitionedCsrFormatTensor]]:
|
|
"""Call the mid level api to do embedding lookup."""
|
|
return self.embedding_lookup(features, weights)
|
|
|
|
# TODO(pineapplejuice233): Duplicated helper function from tpu_embedding_v2.py. Remove
|
|
# this once this file is open souced.
|
|
def _raise_error_for_incorrect_control_flow_context(self):
|
|
"""Raises an error if we are not in the TPUReplicateContext."""
|
|
# Do not allow any XLA control flow (i.e. control flow in between a
|
|
# TPUStrategy's run call and the call to this function), as we can't
|
|
# extract the enqueue from the head when in XLA control flow.
|
|
graph = ops.get_default_graph()
|
|
in_tpu_ctx = False
|
|
while graph is not None:
|
|
ctx = graph._get_control_flow_context() # pylint: disable=protected-access
|
|
while ctx is not None:
|
|
if isinstance(ctx, tpu_replication.TPUReplicateContext):
|
|
in_tpu_ctx = True
|
|
break
|
|
ctx = ctx.outer_context
|
|
if in_tpu_ctx:
|
|
break
|
|
graph = getattr(graph, "outer_graph", None)
|
|
if graph != ops.get_default_graph() and in_tpu_ctx:
|
|
raise RuntimeError(
|
|
"Current graph {} does not match graph which contains "
|
|
"TPUReplicateContext {}. This is most likely due to the fact that "
|
|
"enqueueing embedding data is called inside control flow or a "
|
|
"tf.function inside `strategy.run`. This is not supported because "
|
|
"outside compilation fails to extract the enqueue ops as the head of "
|
|
"a computation.".format(ops.get_default_graph(), graph)
|
|
)
|
|
return in_tpu_ctx
|
|
|
|
@classmethod
|
|
def compute_sparse_core_stats(
|
|
cls,
|
|
features: Any,
|
|
feature_config: Union[FeatureConfig, Iterable], # pylint:disable=g-bare-generic
|
|
num_tpu_chips: int,
|
|
num_sc_per_chip: int = 4,
|
|
optimizer: Optional[tpu_embedding_v2_utils._Optimizer] = None, # pylint:disable=protected-access
|
|
sparse_core_embedding_config: Optional[SparseCoreEmbeddingConfig] = None,
|
|
) -> Tuple[Any, Any]:
|
|
"""Computes the max_ids/unique ids settings from the input features."""
|
|
copy_feature_config = _clone_feature_config(feature_config)
|
|
table_config_list = list(
|
|
{feature.table for feature in nest.flatten(copy_feature_config)}
|
|
)
|
|
|
|
for table in table_config_list:
|
|
if table.optimizer is None:
|
|
table.optimizer = optimizer
|
|
|
|
flat_features = nest.flatten_with_joined_string_paths(copy_feature_config)
|
|
|
|
s = _stack_tables_with_same_table_dim_and_optimizer(
|
|
table_config_list,
|
|
flat_features,
|
|
num_tpu_chips,
|
|
num_sc_per_chip,
|
|
sparse_core_embedding_config,
|
|
)
|
|
|
|
flat_inputs = nest.flatten(features)
|
|
|
|
# First process them to be COO tensors.
|
|
table_to_list_of_coos = (
|
|
TPUEmbeddingV2._preprocess_inputs_and_weights_to_list_of_coo_tensors(
|
|
flat_inputs=flat_inputs,
|
|
flat_weights=[None] * len(flat_inputs),
|
|
flat_features=flat_features,
|
|
stacked_table_to_tables=s.stacked_table_to_tables,
|
|
table_to_stacked_table_offset=s.table_to_stacked_table_offset,
|
|
feature_to_sample_offset=s.feature_to_sample_offset,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
stacked_table_to_sample_count=s.table_to_sample_count,
|
|
num_sc_shards=num_sc_per_chip * num_tpu_chips,
|
|
)
|
|
)
|
|
|
|
table_to_max_ids_per_sparse_core = {
|
|
table_name: 0 for table_name in s.stacked_table_to_tables
|
|
}
|
|
table_to_max_unique_ids_per_sparse_core = {
|
|
table_name: 0 for table_name in s.stacked_table_to_tables
|
|
}
|
|
|
|
for table_name in s.stacked_table_to_tables:
|
|
feature_width = s.stacked_table_to_tables[table_name][0].dim
|
|
|
|
total_vocab_size = sum([
|
|
table.vocabulary_size
|
|
for table in s.stacked_table_to_tables[table_name]
|
|
])
|
|
for i in range(num_sc_per_chip):
|
|
row_ids_list = table_to_list_of_coos[table_name][0][i]
|
|
col_ids_list = table_to_list_of_coos[table_name][1][i]
|
|
gains_list = table_to_list_of_coos[table_name][2][i]
|
|
sample_count_list = table_to_list_of_coos[table_name][3]
|
|
col_offset_list = table_to_list_of_coos[table_name][4]
|
|
|
|
(
|
|
max_ids_per_sparse_core,
|
|
max_unique_ids_per_sparse_core,
|
|
) = xla_ops.get_stats_from_list_of_sparse_core_coo_tensors(
|
|
row_ids_list=row_ids_list,
|
|
col_ids_list=col_ids_list,
|
|
gains_list=gains_list,
|
|
sample_count_list=sample_count_list,
|
|
col_offset_list=col_offset_list,
|
|
num_replica=num_tpu_chips,
|
|
table_vocab_size=total_vocab_size,
|
|
feature_width=feature_width,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
table_name=table_name,
|
|
)
|
|
|
|
table_to_max_ids_per_sparse_core[table_name] = math_ops.maximum(
|
|
table_to_max_ids_per_sparse_core[table_name],
|
|
max_ids_per_sparse_core,
|
|
)
|
|
table_to_max_unique_ids_per_sparse_core[table_name] = math_ops.maximum(
|
|
table_to_max_unique_ids_per_sparse_core[table_name],
|
|
max_unique_ids_per_sparse_core,
|
|
)
|
|
return (
|
|
table_to_max_ids_per_sparse_core,
|
|
table_to_max_unique_ids_per_sparse_core,
|
|
)
|
|
|
|
def enqueue(
|
|
self,
|
|
features: Any,
|
|
weights: Optional[Any] = None,
|
|
device: Optional[str] = None,
|
|
) -> Any:
|
|
"""Preprocessing the features on host."""
|
|
nest.assert_same_structure(self._feature_config, features)
|
|
|
|
flat_inputs = nest.flatten(features)
|
|
flat_weights = [None] * len(flat_inputs)
|
|
if weights is not None:
|
|
nest.assert_same_structure(self._feature_config, weights)
|
|
flat_weights = nest.flatten(weights)
|
|
|
|
in_tpu_context = self._raise_error_for_incorrect_control_flow_context()
|
|
|
|
if in_tpu_context:
|
|
# Automatically apply outside compilation if we are in tpu context.
|
|
return tpu_replication.outside_compilation(
|
|
self._preprocess_features,
|
|
num_replicas_in_sync=self._strategy.num_replicas_in_sync,
|
|
max_ids_per_chip_per_sample=self.max_ids_per_chip_per_sample,
|
|
max_minibatches_per_sc=self.max_minibatches_per_sc,
|
|
num_sc_per_chip=self._num_sc_per_chip,
|
|
num_sc_shards=self._num_sc_shards,
|
|
stacked_table_to_tables=self._stacked_table_to_tables,
|
|
table_to_stacked_table_offset=self._table_to_stacked_table_offset,
|
|
table_to_sample_count=self._table_to_sample_count,
|
|
feature_to_sample_offset=self._feature_to_sample_offset,
|
|
flat_features=self._flat_features,
|
|
flat_inputs=flat_inputs,
|
|
flat_weights=flat_weights,
|
|
)
|
|
elif device is None:
|
|
# This is used by keras function tracing. Use any of the TPU devices
|
|
# and trace once for a single device.
|
|
tpu_devices = self._strategy.extended._tpu_devices # pylint:disable=protected-access
|
|
|
|
with ops.device(device_util.get_host_for_device(tpu_devices[0][0])):
|
|
return self._preprocess_features(
|
|
num_replicas_in_sync=self._strategy.num_replicas_in_sync,
|
|
max_ids_per_chip_per_sample=self.max_ids_per_chip_per_sample,
|
|
max_minibatches_per_sc=self.max_minibatches_per_sc,
|
|
num_sc_per_chip=self._num_sc_per_chip,
|
|
num_sc_shards=self._num_sc_shards,
|
|
stacked_table_to_tables=self._stacked_table_to_tables,
|
|
table_to_stacked_table_offset=self._table_to_stacked_table_offset,
|
|
table_to_sample_count=self._table_to_sample_count,
|
|
feature_to_sample_offset=self._feature_to_sample_offset,
|
|
flat_features=self._flat_features,
|
|
flat_inputs=flat_inputs,
|
|
flat_weights=flat_weights,
|
|
)
|
|
else:
|
|
device_spec = tf_device.DeviceSpec.from_string(device)
|
|
if device_spec.device_type != "TPU":
|
|
raise ValueError("Non-TPU device {} passed to enqueue.".format(device))
|
|
|
|
with ops.device(device_util.get_host_for_device(device)):
|
|
return self._preprocess_features(
|
|
num_replicas_in_sync=self._strategy.num_replicas_in_sync,
|
|
max_ids_per_chip_per_sample=self.max_ids_per_chip_per_sample,
|
|
max_minibatches_per_sc=self.max_minibatches_per_sc,
|
|
num_sc_per_chip=self._num_sc_per_chip,
|
|
num_sc_shards=self._num_sc_shards,
|
|
stacked_table_to_tables=self._stacked_table_to_tables,
|
|
table_to_stacked_table_offset=self._table_to_stacked_table_offset,
|
|
table_to_sample_count=self._table_to_sample_count,
|
|
feature_to_sample_offset=self._feature_to_sample_offset,
|
|
flat_features=self._flat_features,
|
|
flat_inputs=flat_inputs,
|
|
flat_weights=flat_weights,
|
|
)
|
|
|
|
def _copy_tensors_to_device(
|
|
self,
|
|
partitioned_tensors: Dict[str, Any],
|
|
) -> Any:
|
|
"""Copy tensors to device."""
|
|
partitioned_device_tensors = {}
|
|
for table_name in partitioned_tensors:
|
|
partitioned_tensor = partitioned_tensors[table_name][0]
|
|
row_pointers_unpadded_size = partitioned_tensors[table_name][1]
|
|
ids_unpadded_size = partitioned_tensors[table_name][2]
|
|
|
|
row_pointers, sorted_sample_ids, sorted_token_ids, sorted_gains = (
|
|
xla_ops.tpu_copy_with_dynamic_shape(
|
|
[
|
|
partitioned_tensor.row_pointers,
|
|
partitioned_tensor.sorted_sample_ids,
|
|
partitioned_tensor.sorted_token_ids,
|
|
partitioned_tensor.sorted_gains,
|
|
],
|
|
[
|
|
row_pointers_unpadded_size,
|
|
ids_unpadded_size,
|
|
ids_unpadded_size,
|
|
ids_unpadded_size,
|
|
],
|
|
)
|
|
)
|
|
|
|
# Placeholder Op for pipelining.
|
|
row_pointers, sorted_sample_ids, sorted_token_ids, sorted_gains = (
|
|
xla_ops.tpu_annotate_tensors_with_dynamic_shape([
|
|
row_pointers,
|
|
sorted_sample_ids,
|
|
sorted_token_ids,
|
|
sorted_gains,
|
|
])
|
|
)
|
|
|
|
partitioned_device_tensors[table_name] = PartitionedCsrFormatTensor(
|
|
row_pointers=row_pointers,
|
|
sorted_sample_ids=sorted_sample_ids,
|
|
sorted_token_ids=sorted_token_ids,
|
|
sorted_gains=sorted_gains,
|
|
sample_count=partitioned_tensor.sample_count,
|
|
num_minibatches_per_physical_sparse_core=(
|
|
partitioned_tensor.num_minibatches_per_physical_sparse_core
|
|
),
|
|
)
|
|
return partitioned_device_tensors
|
|
|
|
def dequeue(
|
|
self,
|
|
partitioned_tensors: Tuple[
|
|
Dict[str, PartitionedCsrFormatTensor], int, int
|
|
],
|
|
) -> Tuple[Any, Dict[str, PartitionedCsrFormatTensor]]:
|
|
"""Perform embedding lookup."""
|
|
# We expect this dequeue function will always run inside tpu context.
|
|
context = EmbeddingPipeliningContext(
|
|
_PIPELINE_MODE_FORWARD, self._pipelining
|
|
)
|
|
context.Enter()
|
|
partitioned_tensors = tpu_replication.outside_compilation(
|
|
self._copy_tensors_to_device,
|
|
partitioned_tensors=partitioned_tensors,
|
|
)
|
|
|
|
activations = {}
|
|
# Take num_minibatches_per_physical_sparse_core from any table as
|
|
# they are the same across tables.
|
|
num_minibatches_per_physical_sparse_core = list(
|
|
partitioned_tensors.values()
|
|
)[0].num_minibatches_per_physical_sparse_core
|
|
|
|
for table_name in self._stacked_table_to_tables:
|
|
partitioned_tensor = partitioned_tensors[table_name]
|
|
|
|
table = self.variables[table_name]["parameters"]
|
|
quantization_config = self._quantization_configs[table_name]
|
|
if not isinstance(partitioned_tensor, PartitionedCsrFormatTensor):
|
|
raise ValueError(
|
|
"Expect PartitionedCsrFormatTensor but get"
|
|
f" {type(partitioned_tensor)}."
|
|
)
|
|
activation = xla_ops.xla_sparse_dense_matmul_with_static_buffer_size(
|
|
row_pointers=partitioned_tensor.row_pointers,
|
|
sorted_sample_ids=partitioned_tensor.sorted_sample_ids,
|
|
sorted_token_ids=partitioned_tensor.sorted_token_ids,
|
|
sorted_gains=partitioned_tensor.sorted_gains,
|
|
input_size=self._table_to_sample_count[table_name],
|
|
embedding_table=table,
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
quantization_config_low=(
|
|
quantization_config.lower if quantization_config else 0
|
|
),
|
|
quantization_config_high=(
|
|
quantization_config.upper if quantization_config else 0
|
|
),
|
|
quantization_config_num_buckets=(
|
|
quantization_config.num_buckets if quantization_config else 0
|
|
),
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
|
|
activations[table_name] = activation
|
|
|
|
context.Exit()
|
|
# Note that unstacking gradients is placed on the core of the trianing step
|
|
# to reduce the number of input/output arguments of the training loop during
|
|
# pipelining.
|
|
activations = self._unstack_activations(activations)
|
|
|
|
return (activations, partitioned_tensors)
|
|
|
|
def embedding_lookup(
|
|
self, features: Any, weights: Optional[Any] = None
|
|
) -> Tuple[Any, Dict[str, PartitionedCsrFormatTensor]]:
|
|
"""Perform embedding lookup on the input feature.
|
|
|
|
Args:
|
|
features: A nested structure of `tf.Tensor`s, `tf.SparseTensor`s or
|
|
`tf.RaggedTensor`s, with the same structure as `feature_config`. Inputs
|
|
will be downcast to `tf.int32`. Only one type out of `tf.SparseTensor`
|
|
or `tf.RaggedTensor` is supported per call.
|
|
weights: If not `None`, a nested structure of `tf.Tensor`s,
|
|
`tf.SparseTensor`s or `tf.RaggedTensor`s, matching the above, except
|
|
that the tensors should be of float type (and they will be downcast to
|
|
`tf.float32`). For `tf.SparseTensor`s we assume the `indices` are the
|
|
same for the parallel entries from `features` and similarly for
|
|
`tf.RaggedTensor`s we assume the row_splits are the same.
|
|
|
|
Raises:
|
|
ValueError: If the input feature is not one of the Tensor, SparseTensor or
|
|
RaggedTensor type.
|
|
TypeError: If the type of any sequence in `features` does not match
|
|
corresponding sequence in `feature_config`. Similarly for `weights`, if
|
|
not `None`.
|
|
|
|
Returns:
|
|
packed_activations: Embedding lookup results packed as the same sequence
|
|
of the input feature.
|
|
packed_output: A dict of PartitionedCsrFormatTensors.
|
|
"""
|
|
if not self._built:
|
|
self._maybe_build()
|
|
|
|
context = EmbeddingPipeliningContext(
|
|
_PIPELINE_MODE_FORWARD, self._pipelining
|
|
)
|
|
context.Enter()
|
|
|
|
partitioned_tensors = self.enqueue(features, weights)
|
|
|
|
context.Exit()
|
|
|
|
result = self.dequeue(partitioned_tensors)
|
|
|
|
return result
|
|
|
|
def _preprocess_features(
|
|
self,
|
|
num_replicas_in_sync: int,
|
|
max_ids_per_chip_per_sample: int,
|
|
max_minibatches_per_sc: int,
|
|
num_sc_per_chip: int,
|
|
num_sc_shards: int,
|
|
stacked_table_to_tables: Dict[str, Any],
|
|
table_to_stacked_table_offset: Dict[str, Tuple[str, int, int]],
|
|
table_to_sample_count: Dict[str, int],
|
|
feature_to_sample_offset: Dict[str, int],
|
|
flat_features: Any,
|
|
flat_inputs: Any,
|
|
flat_weights: Optional[Any] = None,
|
|
) -> Any:
|
|
"""Function to preprocess features."""
|
|
# Preprocess the inputs into list of COO tensor.
|
|
table_to_list_of_coos = (
|
|
TPUEmbeddingV2._preprocess_inputs_and_weights_to_list_of_coo_tensors(
|
|
flat_inputs,
|
|
flat_weights,
|
|
flat_features,
|
|
stacked_table_to_tables,
|
|
table_to_stacked_table_offset,
|
|
feature_to_sample_offset,
|
|
num_sc_per_chip,
|
|
table_to_sample_count,
|
|
num_sc_shards,
|
|
)
|
|
)
|
|
|
|
# Sort the COO tensors.
|
|
table_to_sorted_coo_tensor = self._sort_list_of_coo_tensors(
|
|
num_replicas_in_sync,
|
|
table_to_list_of_coos,
|
|
stacked_table_to_tables,
|
|
num_sc_per_chip,
|
|
)
|
|
|
|
table_to_csr_format_tensor = (
|
|
self._get_csr_wrapped_coo_from_sorted_coo_tensor(
|
|
num_replicas_in_sync,
|
|
max_ids_per_chip_per_sample,
|
|
max_minibatches_per_sc,
|
|
table_to_sorted_coo_tensor,
|
|
stacked_table_to_tables,
|
|
table_to_sample_count,
|
|
num_sc_per_chip,
|
|
)
|
|
)
|
|
|
|
return table_to_csr_format_tensor
|
|
|
|
@classmethod
|
|
def _convert_input_feature_to_list_of_coo_tensors(
|
|
cls,
|
|
input_feature: Union[
|
|
tensor.Tensor, sparse_tensor.SparseTensor, ragged_tensor.RaggedTensor
|
|
],
|
|
weight: Optional[tensor.Tensor],
|
|
feature_config: tpu_embedding_v2_utils.FeatureConfig,
|
|
row_offset: int,
|
|
col_offset: int,
|
|
col_shift: int,
|
|
unused_vocab_size: int,
|
|
num_sc_per_chip: int,
|
|
num_sc_shards: int,
|
|
stacked_table_sample_count: int,
|
|
) -> Any:
|
|
"""Convert any of the expected input types to a COO format."""
|
|
sample_count = functools.reduce(operator.mul, feature_config.output_shape)
|
|
if isinstance(input_feature, tensor.Tensor):
|
|
input_feature = array_ops.reshape(input_feature, [-1])
|
|
if weight is None:
|
|
weight = array_ops.ones_like(input_feature, dtype=dtypes.float32)
|
|
elif isinstance(weight, tensor.Tensor):
|
|
weight = array_ops.reshape(weight, [-1])
|
|
else:
|
|
raise ValueError(
|
|
f"Expect weight to be Tensor type but got {type(weight)}"
|
|
)
|
|
row_ids_list, col_ids_list, gains_list = (
|
|
xla_ops.convert_to_list_of_sparse_core_coo_tensors(
|
|
indices_or_row_splits=array_ops.zeros((0,), dtype=dtypes.int32),
|
|
values=math_ops.cast(input_feature, dtype=dtypes.int32),
|
|
weights=math_ops.cast(weight, dtypes.float32),
|
|
sample_count=sample_count,
|
|
combiner=feature_config.table.combiner,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
row_offset=row_offset,
|
|
col_offset=col_offset,
|
|
col_shift=col_shift,
|
|
num_sc_shards=num_sc_shards,
|
|
stacked_table_sample_count=stacked_table_sample_count,
|
|
)
|
|
)
|
|
elif isinstance(input_feature, sparse_tensor.SparseTensor):
|
|
if weight is None:
|
|
weight = array_ops.ones_like(input_feature.values, dtype=dtypes.float32)
|
|
elif isinstance(weight, sparse_tensor.SparseTensor):
|
|
weight = weight.values
|
|
else:
|
|
raise ValueError(
|
|
f"Expect weight to be SparseTensor type but got {type(weight)}"
|
|
)
|
|
row_ids_list, col_ids_list, gains_list = (
|
|
xla_ops.convert_to_list_of_sparse_core_coo_tensors(
|
|
indices_or_row_splits=math_ops.cast(
|
|
input_feature.indices, dtype=dtypes.int32
|
|
),
|
|
values=math_ops.cast(input_feature.values, dtype=dtypes.int32),
|
|
weights=math_ops.cast(weight, dtypes.float32),
|
|
sample_count=sample_count,
|
|
combiner=feature_config.table.combiner,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
row_offset=row_offset,
|
|
col_offset=col_offset,
|
|
col_shift=col_shift,
|
|
num_sc_shards=num_sc_shards,
|
|
stacked_table_sample_count=stacked_table_sample_count,
|
|
)
|
|
)
|
|
elif isinstance(input_feature, ragged_tensor.RaggedTensor):
|
|
if weight is None:
|
|
weight = array_ops.ones_like(input_feature.values, dtype=dtypes.float32)
|
|
elif isinstance(weight, ragged_tensor.RaggedTensor):
|
|
weight = weight.values
|
|
else:
|
|
raise ValueError(
|
|
f"Expect weight to be RaggedTensor type but got {type(weight)}"
|
|
)
|
|
row_ids_list, col_ids_list, gains_list = (
|
|
xla_ops.convert_to_list_of_sparse_core_coo_tensors(
|
|
indices_or_row_splits=math_ops.cast(
|
|
input_feature.row_splits, dtype=dtypes.int32
|
|
),
|
|
values=math_ops.cast(input_feature.values, dtype=dtypes.int32),
|
|
weights=math_ops.cast(weight, dtypes.float32),
|
|
sample_count=sample_count,
|
|
combiner=feature_config.table.combiner,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
row_offset=row_offset,
|
|
col_offset=col_offset,
|
|
col_shift=col_shift,
|
|
num_sc_shards=num_sc_shards,
|
|
stacked_table_sample_count=stacked_table_sample_count,
|
|
)
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"Input of unknown type {type(input_feature)}. Please only pass "
|
|
"Tensor, SparseTensor or RaggedTensor as input to embedding "
|
|
"lookup."
|
|
)
|
|
return row_ids_list, col_ids_list, gains_list, sample_count
|
|
|
|
@classmethod
|
|
def _preprocess_inputs_and_weights_to_list_of_coo_tensors(
|
|
cls,
|
|
flat_inputs: Any,
|
|
flat_weights: Any,
|
|
flat_features: Any,
|
|
stacked_table_to_tables: Dict[str, Any],
|
|
table_to_stacked_table_offset: Dict[str, Tuple[str, int, int]],
|
|
feature_to_sample_offset: Dict[str, int],
|
|
num_sc_per_chip: int,
|
|
stacked_table_to_sample_count: Dict[str, int],
|
|
num_sc_shards: int,
|
|
) -> Dict[str, Any]:
|
|
"""Convert the raw inputs into list of coo tensors."""
|
|
table_to_list_of_coos = { # pylint: disable=g-complex-comprehension
|
|
table_name: (
|
|
[[], [], [], []],
|
|
[[], [], [], []],
|
|
[[], [], [], []],
|
|
[],
|
|
[],
|
|
)
|
|
for table_name in stacked_table_to_tables
|
|
}
|
|
for inp, weight, (feature_path, feature) in zip(
|
|
flat_inputs, flat_weights, flat_features
|
|
):
|
|
table_name, col_offset, col_shift = table_to_stacked_table_offset[
|
|
feature.table.name
|
|
]
|
|
stacked_table_sample_count = stacked_table_to_sample_count[table_name]
|
|
row_offset = feature_to_sample_offset[feature_path]
|
|
# Consider making this into one op per table rather than per feature?
|
|
row_ids_list, col_ids_list, gains_list, sample_count = (
|
|
TPUEmbeddingV2._convert_input_feature_to_list_of_coo_tensors(
|
|
inp,
|
|
weight,
|
|
feature,
|
|
row_offset,
|
|
col_offset,
|
|
col_shift,
|
|
feature.table.vocabulary_size,
|
|
num_sc_per_chip,
|
|
num_sc_shards,
|
|
stacked_table_sample_count,
|
|
)
|
|
)
|
|
for i in range(num_sc_per_chip):
|
|
table_to_list_of_coos[table_name][0][i].append(row_ids_list[i])
|
|
table_to_list_of_coos[table_name][1][i].append(col_ids_list[i])
|
|
table_to_list_of_coos[table_name][2][i].append(gains_list[i])
|
|
table_to_list_of_coos[table_name][3].append(
|
|
sample_count // num_sc_per_chip
|
|
)
|
|
table_to_list_of_coos[table_name][4].append(col_offset)
|
|
return table_to_list_of_coos
|
|
|
|
def _sort_list_of_coo_tensors(
|
|
self,
|
|
num_replicas_in_sync: int,
|
|
table_to_list_of_coos: Dict[str, Any],
|
|
stacked_table_to_tables: Dict[str, Any],
|
|
num_sc_per_chip: int,
|
|
) -> Tuple[Dict[str, Any], List[tensor.Tensor]]:
|
|
"""Sort the coo tensors by replica."""
|
|
table_to_sorted_coo_tensor = {
|
|
table_name: ([], [], [], []) for table_name in stacked_table_to_tables
|
|
}
|
|
for table_name in stacked_table_to_tables:
|
|
# Feature width are the same across stacked tables.
|
|
feature_width = stacked_table_to_tables[table_name][0].dim
|
|
|
|
total_vocab_size = sum([
|
|
table.vocabulary_size for table in stacked_table_to_tables[table_name]
|
|
])
|
|
for i in range(num_sc_per_chip):
|
|
row_ids_list = table_to_list_of_coos[table_name][0][i]
|
|
col_ids_list = table_to_list_of_coos[table_name][1][i]
|
|
gains_list = table_to_list_of_coos[table_name][2][i]
|
|
sample_count_list = table_to_list_of_coos[table_name][3]
|
|
col_offset_list = table_to_list_of_coos[table_name][4]
|
|
|
|
(
|
|
sorted_row_ids,
|
|
sorted_col_ids,
|
|
sorted_gains,
|
|
id_counts,
|
|
) = xla_ops.sort_list_of_sparse_core_coo_tensors(
|
|
row_ids_list=row_ids_list,
|
|
col_ids_list=col_ids_list,
|
|
gains_list=gains_list,
|
|
sample_count_list=sample_count_list,
|
|
col_offset_list=col_offset_list,
|
|
num_replica=num_replicas_in_sync,
|
|
table_vocab_size=total_vocab_size,
|
|
feature_width=feature_width,
|
|
num_sc_per_chip=num_sc_per_chip,
|
|
max_ids_per_sparse_core=self._table_to_max_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
max_unique_ids_per_sparse_core=self._table_to_max_unique_ids_per_sparse_core[
|
|
table_name
|
|
],
|
|
table_name=table_name,
|
|
)
|
|
|
|
table_to_sorted_coo_tensor[table_name][0].append(sorted_row_ids)
|
|
table_to_sorted_coo_tensor[table_name][1].append(sorted_col_ids)
|
|
table_to_sorted_coo_tensor[table_name][2].append(sorted_gains)
|
|
table_to_sorted_coo_tensor[table_name][3].append(id_counts)
|
|
|
|
return table_to_sorted_coo_tensor
|
|
|
|
def _get_csr_wrapped_coo_from_sorted_coo_tensor(
|
|
self,
|
|
num_replicas_in_sync: int,
|
|
max_ids_per_chip_per_sample: int,
|
|
max_minibatches_per_sc: int,
|
|
table_to_sorted_coo_tensor: Dict[str, Any],
|
|
stacked_table_to_tables: Dict[str, Any],
|
|
table_to_sample_count: Dict[str, int],
|
|
num_sc_per_chip: int,
|
|
) -> Any:
|
|
"""Get csr wrapped coo tensor from the sorted coo tensor."""
|
|
table_to_csr_format_tensor = {}
|
|
for table_name in stacked_table_to_tables:
|
|
(
|
|
sorted_row_ids_list,
|
|
sorted_col_ids_list,
|
|
sorted_gains_list,
|
|
id_counts_list,
|
|
) = table_to_sorted_coo_tensor[table_name]
|
|
|
|
# Feature width are the same across stacked tables.
|
|
feature_width = stacked_table_to_tables[table_name][0].dim
|
|
|
|
total_vocab_size = sum([
|
|
table.vocabulary_size for table in stacked_table_to_tables[table_name]
|
|
])
|
|
(
|
|
row_pointers,
|
|
sorted_sample_ids,
|
|
sorted_token_ids,
|
|
sorted_gains,
|
|
row_pointers_unpadded_size,
|
|
ids_unpadded_size,
|
|
num_minibatches_per_physical_sparse_core,
|
|
) = xla_ops.convert_to_sparse_core_csr_wrapped_coo_tensor(
|
|
sorted_row_ids_list=sorted_row_ids_list,
|
|
sorted_col_ids_list=sorted_col_ids_list,
|
|
sorted_gains_list=sorted_gains_list,
|
|
id_counts_list=id_counts_list,
|
|
splits=constant_op.constant(
|
|
0, dtype=dtypes.int64
|
|
), # no splits are needed.
|
|
sample_count_per_sc=table_to_sample_count[table_name]
|
|
// num_sc_per_chip,
|
|
num_replica=num_replicas_in_sync,
|
|
max_minibatches_per_sc=max_minibatches_per_sc,
|
|
max_ids_per_chip_per_sample=max_ids_per_chip_per_sample,
|
|
table_vocab_size=total_vocab_size,
|
|
feature_width=feature_width,
|
|
table_name=table_name,
|
|
allow_id_dropping=self._sparse_core_embedding_config.allow_id_dropping,
|
|
)
|
|
table_to_csr_format_tensor[table_name] = (
|
|
PartitionedCsrFormatTensor(
|
|
row_pointers=row_pointers,
|
|
sorted_sample_ids=sorted_sample_ids,
|
|
sorted_token_ids=sorted_token_ids,
|
|
sorted_gains=sorted_gains,
|
|
sample_count=table_to_sample_count[table_name],
|
|
num_minibatches_per_physical_sparse_core=num_minibatches_per_physical_sparse_core,
|
|
),
|
|
row_pointers_unpadded_size,
|
|
ids_unpadded_size,
|
|
)
|
|
return table_to_csr_format_tensor
|
|
|
|
def _unstack_activations(self, activations: Dict[str, tensor.Tensor]):
|
|
"""Untack the incoming per table activations into per feature."""
|
|
|
|
# Activations are stacked in a particular order. That order is the order
|
|
# features appear in the self._flat_features.
|
|
flattened_activations = []
|
|
table_to_current_offset = {
|
|
table_name: 0 for table_name in self._stacked_table_to_tables
|
|
}
|
|
for table_name in self._stacked_table_to_tables:
|
|
activation_shape = activations[table_name].shape
|
|
activations[table_name] = array_ops.reshape(
|
|
activations[table_name],
|
|
[self._num_sc_per_chip, -1, activation_shape[-1]],
|
|
)
|
|
for _, feature in self._flat_features:
|
|
sample_count = functools.reduce(operator.mul, feature.output_shape)
|
|
table_name = self._table_to_stacked_table_offset[feature.table.name][0]
|
|
extra_cols = self._table_to_padding_columns[feature.table.name]
|
|
activation = array_ops.slice(
|
|
activations[table_name],
|
|
[0, table_to_current_offset[table_name], 0],
|
|
[
|
|
self._num_sc_per_chip,
|
|
sample_count // self._num_sc_per_chip,
|
|
feature.table.dim - extra_cols,
|
|
],
|
|
)
|
|
# Reshape to follow the user's requested output shape.
|
|
activation = array_ops.reshape(
|
|
activation,
|
|
list(feature.output_shape) + [feature.table.dim - extra_cols],
|
|
)
|
|
flattened_activations.append(activation)
|
|
table_to_current_offset[table_name] += (
|
|
sample_count // self._num_sc_per_chip
|
|
)
|
|
|
|
return nest.pack_sequence_as(self._feature_config, flattened_activations)
|
|
|
|
def _stack_gradients(self, gradients):
|
|
"""Stack the incoming gradients to per table gradients."""
|
|
|
|
# Gradients are stacked in a particular order. That order is the order
|
|
# features appear in the self._flat_features.
|
|
table_to_gradient_list = {
|
|
table_name: [[], [], [], []]
|
|
for table_name in self._stacked_table_to_tables
|
|
}
|
|
flattend_gradients = nest.flatten(gradients)
|
|
for gradient, (path, feature) in zip(
|
|
flattend_gradients, self._flat_features
|
|
):
|
|
sample_count = functools.reduce(operator.mul, feature.output_shape)
|
|
if gradient is not None and not isinstance(gradient, tensor.Tensor):
|
|
raise ValueError(
|
|
f"found non-tensor type: {type(gradient)} at path {path}."
|
|
)
|
|
if gradient is None:
|
|
# TODO(bfontain): In the case that an entire table's gradient is gone
|
|
# then maybe we can just omit the update all together?
|
|
logging.warning(
|
|
(
|
|
"No gradient passed for feature %s, sending zero "
|
|
"gradient. This may not be correct behavior for certain "
|
|
"optimizers like Adam."
|
|
),
|
|
path,
|
|
)
|
|
gradient = array_ops.zeros(
|
|
(sample_count, feature.table.dim), dtype=dtypes.float32
|
|
)
|
|
table_name = self._table_to_stacked_table_offset[feature.table.name][0]
|
|
extra_cols = self._table_to_padding_columns[feature.table.name]
|
|
gradient = array_ops.reshape(
|
|
gradient, [-1, feature.table.dim - extra_cols]
|
|
)
|
|
if extra_cols != 0:
|
|
gradient = array_ops.pad(gradient, [[0, 0], [0, extra_cols]])
|
|
# Ensure static shape after padding.
|
|
gradient.set_shape([sample_count, feature.table.dim])
|
|
per_sc_sample_count = sample_count // self._num_sc_per_chip
|
|
for i in range(self._num_sc_per_chip):
|
|
table_to_gradient_list[table_name][i].append(
|
|
array_ops.slice(
|
|
gradient,
|
|
[i * per_sc_sample_count, 0],
|
|
[
|
|
per_sc_sample_count,
|
|
feature.table.dim,
|
|
],
|
|
)
|
|
)
|
|
for table_name in table_to_gradient_list:
|
|
table_to_gradient_list[table_name] = array_ops.concat(
|
|
[
|
|
array_ops.concat(table_to_gradient_list[table_name][i], axis=0)
|
|
for i in range(self._num_sc_per_chip)
|
|
],
|
|
axis=0,
|
|
)
|
|
return table_to_gradient_list
|
|
|
|
|
|
# TODO(pineapplejuice233): Merge this function with the one in tpu_embeding_v2.py once
|
|
# this file is OSSed.
|
|
def extract_variable_info(
|
|
kwargs: Any,
|
|
) -> Tuple[
|
|
str, Tuple[int, ...], dtypes.DType, Callable[[], Any], Optional[int]
|
|
]:
|
|
"""Extracts the variable creation attributes from the kwargs.
|
|
|
|
Args:
|
|
kwargs: a dict of keyword arguments that were passed to a variable creator
|
|
scope.
|
|
|
|
Returns:
|
|
A tuple of variable name, shape, dtype, initialization function,
|
|
restore_uid.
|
|
"""
|
|
|
|
def get_restore_uid(initial_value: Callable[..., Any]) -> int | None:
|
|
return getattr(initial_value, "restore_uid", None)
|
|
|
|
if isinstance(kwargs["initial_value"], functools.partial) and (
|
|
"shape" in kwargs["initial_value"].keywords
|
|
or kwargs["initial_value"].args
|
|
):
|
|
# Sometimes shape is passed positionally, sometimes it's passed as a kwarg.
|
|
if "shape" in kwargs["initial_value"].keywords:
|
|
shape = kwargs["initial_value"].keywords["shape"]
|
|
else:
|
|
shape = kwargs["initial_value"].args[0]
|
|
return (
|
|
kwargs["name"],
|
|
shape,
|
|
kwargs["initial_value"].keywords.get("dtype", kwargs["dtype"]),
|
|
kwargs["initial_value"].func,
|
|
get_restore_uid(kwargs["initial_value"].func),
|
|
)
|
|
elif (
|
|
"shape" not in kwargs
|
|
or kwargs["shape"] is None
|
|
or not callable(kwargs["initial_value"])
|
|
):
|
|
raise ValueError(
|
|
"Unable to extract initializer function and shape from {}. Please "
|
|
"either pass a function that expects a shape and dtype as the "
|
|
"initial value for your variable or functools.partial object with "
|
|
"the shape and dtype kwargs set. This is needed so that we can "
|
|
"initialize the shards of the ShardedVariable locally.".format(
|
|
kwargs["initial_value"]
|
|
)
|
|
)
|
|
else:
|
|
return (
|
|
kwargs["name"],
|
|
kwargs["shape"],
|
|
kwargs["dtype"],
|
|
kwargs["initial_value"],
|
|
get_restore_uid(kwargs["initial_value"]),
|
|
)
|
|
|
|
|
|
def is_checkpoint_initial_value(initial_value: Any) -> bool:
|
|
"""Whether the initial value is from checkpoint."""
|
|
return (
|
|
isinstance(initial_value, base.CheckpointInitialValue)
|
|
or isinstance(initial_value, base.CheckpointInitialValueCallable)
|
|
or (
|
|
isinstance(initial_value, functools.partial)
|
|
and isinstance(
|
|
initial_value.func, base.CheckpointInitialValueCallable
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def make_sharded_variable_creator(
|
|
strategy: distribute_lib.Strategy,
|
|
) -> Callable[..., Any]:
|
|
"""Create a variable creator which shards across all the tpu device.
|
|
|
|
Args:
|
|
strategy: a TPUStrategy object.
|
|
|
|
Returns:
|
|
The sharded variable creator.
|
|
"""
|
|
tpu_devices = strategy.extended._tpu_devices # pylint:disable=protected-access
|
|
|
|
def _create_sharded_variable(next_creator, *args, **kwargs):
|
|
"""Create a TPUEmbeddingShardedVariable."""
|
|
# Avoid the default mirror variable creator.
|
|
kwargs["skip_mirrored_creator"] = True
|
|
|
|
# Only support sharding on the first dimension.
|
|
shard_dim = 0
|
|
|
|
num_replicas, num_cores_per_replica = tpu_devices.shape
|
|
|
|
is_ckpt_init_value = is_checkpoint_initial_value(kwargs["initial_value"])
|
|
|
|
arg_spec = tf_inspect.getfullargspec(kwargs["initial_value"])
|
|
if (
|
|
is_ckpt_init_value
|
|
and "shard_info" not in arg_spec.args
|
|
and "shard_info" not in arg_spec.kwonlyargs
|
|
):
|
|
raise ValueError(
|
|
"When a sharded variable is initialized from a checkpoint, "
|
|
"shard_info must be in arguments of the init function."
|
|
)
|
|
|
|
name, shape, dtype, unwrapped_initial_value, restore_uid = (
|
|
extract_variable_info(kwargs)
|
|
)
|
|
|
|
shape = ops.tensor_shape.TensorShape(shape)
|
|
num_devices = num_replicas * num_cores_per_replica
|
|
# NOTE: only support sharding variables evenly across devices.
|
|
if shape[shard_dim] % num_devices != 0:
|
|
raise ValueError(
|
|
"Only evenly sharding across devices is currently supported. "
|
|
"Got shape {} and {} devices".format(shape, num_devices)
|
|
)
|
|
|
|
partition_shape = shape.as_list()
|
|
partition_shape[shard_dim] = partition_shape[shard_dim] // num_devices
|
|
|
|
unwrapped_arg_spec = tf_inspect.getargspec(unwrapped_initial_value)
|
|
sharding_aware = "shard_info" in unwrapped_arg_spec.args
|
|
|
|
variables = []
|
|
# Keep track of offset for sharding aware initializers.
|
|
partition_offset = [0] * len(shape)
|
|
for replica_id in range(num_replicas):
|
|
for logic_core_id in range(num_cores_per_replica):
|
|
with ops.device(tpu_devices[replica_id][logic_core_id]):
|
|
kwargs["name"] = f"{name}/{replica_id}"
|
|
kwargs["shape"] = partition_shape
|
|
if sharding_aware:
|
|
# TODO(pineapplejuice233): Change this to use MOD sharding logic.
|
|
shard_info = base.ShardInfo(
|
|
tensor_shape.as_shape(partition_shape),
|
|
copy.deepcopy(partition_offset),
|
|
)
|
|
kwargs["initial_value"] = functools.partial(
|
|
kwargs["initial_value"], shard_info=shard_info
|
|
)
|
|
partition_offset[shard_dim] += partition_shape[shard_dim]
|
|
else:
|
|
kwargs["initial_value"] = functools.partial(
|
|
unwrapped_initial_value, shape=partition_shape, dtype=dtype
|
|
)
|
|
variables.append(next_creator(*args, **kwargs))
|
|
|
|
result = TPUEmbeddingShardedVariable(
|
|
strategy, variables, tf_variables.VariableAggregation.NONE, None
|
|
)
|
|
if restore_uid is not None:
|
|
result._maybe_initialize_trackable() # pylint: disable=protected-access
|
|
result._update_uid = restore_uid # pylint: disable=protected-access
|
|
return result
|
|
|
|
return _create_sharded_variable
|