Files
tensorflow--tensorflow/tensorflow/python/checkpoint/sharding/sharding_policies.py
T
wehub-resource-sync 8a852e4b4e
cffconvert / validate (push) Has been skipped
License Check / license-check (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:14:16 +08:00

354 lines
15 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.
# ==============================================================================
"""Checkpoint policies that determine how tensors are split into shards."""
import math
import operator
from typing import MutableSequence, Sequence
from absl import logging
from tensorflow.python.checkpoint.sharding import sharding_util
from tensorflow.python.eager import context
from tensorflow.python.framework import device as device_lib
from tensorflow.python.framework import dtypes
from tensorflow.python.framework import ops
from tensorflow.python.framework import tensor as tensor_lib
from tensorflow.python.framework import tensor_shape
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import string_ops
from tensorflow.python.ops import variables
from tensorflow.python.trackable import base
from tensorflow.python.util import tf_export
@tf_export.tf_export("train.experimental.ShardByTaskPolicy")
class ShardByTaskPolicy(sharding_util.ShardingCallback):
"""Policy that splits tensors into shards based on their device spec task."""
@property
def description(self) -> str:
return "Split tensors into shards based on their device spec task."
def __call__(
self,
shardable_tensors: Sequence[sharding_util.ShardableTensor]
) -> Sequence[sharding_util.Shard]:
"""Callback to split tensors into shards based on their device spec task.
Args:
shardable_tensors: A list of ShardableTensors.
Returns:
List of shard dicts containing tensors.
[ {checkpoint key: {slice_spec: tensor} } ]
"""
tensors_by_task = {}
for shardable_tensor in shardable_tensors:
tensor = shardable_tensor.tensor
checkpoint_key = shardable_tensor.checkpoint_key
slice_spec = shardable_tensor.slice_spec
(tensors_by_task
.setdefault(checkpoint_key, {})[slice_spec]) = tensor
return [tensors_by_task]
_OffsetAndShape = tuple[Sequence[int], Sequence[int]]
@tf_export.tf_export("train.experimental.MaxShardSizePolicy")
class MaxShardSizePolicy(sharding_util.ShardingCallback):
"""Policy that splits tensors into shards with a max shard size.
Shards may exceed the max shard size if they contain 1. a single scalar/string
tensor that could not be sliced and exceeds the max shard size or 2. the
checkpoint object graph, whose size cannot be calculated when saving.
"""
class MaxShardSizePartitioner():
"""Partition tensors into shards with a max shard size."""
max_shard_size: int
_large_scalars: MutableSequence[sharding_util.Shard]
_tensors_by_shard: MutableSequence[sharding_util.Shard]
_shard_size_remaining: int
_checkpoint_key: str
_dtype: dtypes.DType
_device: device_lib.DeviceSpec
_root_tensor: tensor_lib.Tensor
_slice_spec: variables.Variable.SaveSliceInfo
_full_shape: tensor_shape.TensorShape
_root_shape: tensor_shape.TensorShape
_root_offset: Sequence[int]
_dtype_size: int
_working_tensor_offset: MutableSequence[float]
_working_tensor_shape: tensor_shape.TensorShape
def _get_next_partition(self) -> tuple[int, float]:
"""Gets tensor partition with size closest to shard_size_remaining.
Returns:
A tuple containing the axis and size of the next partition.
"""
rank = self._working_tensor_shape.rank
if rank is None or rank == 0:
return 0, math.inf
num_elems = self._working_tensor_shape.num_elements()
def num_partitions(axis: int) -> float:
axis_len = self._working_tensor_shape.dims[axis].value
slice_elems = num_elems // axis_len
bytes_per_slice = slice_elems * self._dtype_size
slices_per_shard = self._shard_size_remaining // bytes_per_slice
if slices_per_shard == 0:
return math.inf
return math.ceil(axis_len / slices_per_shard)
# Find axis with minimum partitions. (axis with maximum partition size)
# (max partition size is as close as possible to the shard_size_remaining)
min_parts = num_partitions(0)
min_axis = 0
for axis in range(1, rank):
parts_along_axis = num_partitions(axis)
part_size = num_elems * self._dtype_size / parts_along_axis
if (parts_along_axis < min_parts and
part_size <= self._shard_size_remaining):
min_axis, min_parts = axis, int(parts_along_axis)
return (min_axis,
math.ceil(int(self._working_tensor_shape[min_axis]) / min_parts))
def _add_partition(self, part_axis: int, part_size: float):
"""Adds the tensor partition to the shard, if possible.
Args:
part_axis: The axis of the partition.
part_size: The size of the partition.
Raises:
RuntimeError: When the slice size is larger than the remaining shard
size.
"""
# Add what we can to the current shard.
relative_offset = list(
map(operator.sub, self._working_tensor_offset, self._root_offset))
slice_shape = list(map(operator.sub, self._root_shape, relative_offset))
slice_shape[part_axis] = part_size
slice_size_in_bytes = int(math.prod(slice_shape)) * self._dtype_size
with ops.device(self._device):
tensor_slice = array_ops.slice(
self._root_tensor, begin=relative_offset, size=slice_shape)
slice_spec = variables.Variable.SaveSliceInfo(
full_name=self._checkpoint_key,
full_shape=self._full_shape,
var_offset=self._working_tensor_offset,
var_shape=slice_shape).spec.strip()
if slice_size_in_bytes > self.max_shard_size:
logging.warning("Tensor %s's minimum slice %s has size %s bytes and "
"cannot be partitioned into a shard of max shard size "
"%s bytes. It will be added as an individual shard "
"that exceeds the max shard size.",
self._checkpoint_key, slice_spec, slice_size_in_bytes,
self.max_shard_size)
self._large_scalars.append(
{self._checkpoint_key: {slice_spec: tensor_slice}})
elif slice_size_in_bytes > self._shard_size_remaining:
raise RuntimeError(
f"Slice size ({slice_size_in_bytes} bytes) is larger than the "
f"remaining shard size ({self._shard_size_remaining} bytes). This "
"should have been caught in MaxShardSizePolicy._add_partition().")
else:
(self._tensors_by_shard[-1]
.setdefault(self._checkpoint_key, {})[slice_spec]) = tensor_slice
self._shard_size_remaining -= slice_size_in_bytes
if self._shard_size_remaining == 0:
self._tensors_by_shard.append({})
self._shard_size_remaining = self.max_shard_size
# Get remaining portion of tensor to add to the next shard(s).
self._working_tensor_offset[part_axis] += part_size
relative_offset[part_axis] += part_size
self._working_tensor_shape = tensor_shape.TensorShape(list(
map(operator.sub, self._root_shape, relative_offset)))
def get_shards(
self,
max_shard_size: int,
shardable_tensors: Sequence[sharding_util.ShardableTensor]
) -> Sequence[sharding_util.Shard]:
"""Callback to split tensors into shards with a max shard size.
Args:
max_shard_size: The maximum size of a shard file in bytes.
shardable_tensors: A list of ShardableTensors.
Returns:
List of shard dicts containing tensors.
[ {checkpoint key: {slice_spec: tensor} } ]
"""
self.max_shard_size = max_shard_size
self._tensors_by_shard = [{}]
self._large_scalars = []
string_size_warning_printed = False
self._shard_size_remaining = self.max_shard_size
for shardable_tensor in shardable_tensors:
self._checkpoint_key = shardable_tensor.checkpoint_key
self._dtype = shardable_tensor.dtype
self._device = shardable_tensor.device
self._root_tensor = shardable_tensor.tensor
self._slice_spec = shardable_tensor.slice_spec
# If the tensor has already been sliced, make sure to keep track of its
# parent tensor's shape & offset. These will be used when creating slice
# specs later.
if self._slice_spec:
save_slice_info = variables.Variable.SaveSliceInfo.from_spec(
self._slice_spec)
self._full_shape = tensor_shape.TensorShape(
save_slice_info.full_shape)
self._root_shape = tensor_shape.TensorShape(save_slice_info.var_shape)
self._root_offset = save_slice_info.var_offset
else:
self._full_shape = self._root_shape = shardable_tensor.shape
self._root_offset = [0] * self._root_shape.rank
self._dtype_size = dtypes.as_dtype(self._dtype).size
total_size = self._root_shape.num_elements() * self._dtype_size # bytes
# Calculate string tensor sizes.
if self._checkpoint_key == base.OBJECT_GRAPH_PROTO_KEY:
# In graph mode, the object graph is populated using feed_additions
# when the session is run. So, we can't calculate the size here.
# Fortunately, the serialized object graph string will never be that
# big, so we just place it in the current shard without worrying about
# its size.
total_size = self._dtype_size = 0
elif self._dtype == dtypes.variant:
# Can't determine a variant's type, so can't calculate its size.
total_size = self._dtype_size = 0
elif (self._dtype == dtypes.string
and not context.executing_eagerly()
and ops.get_default_session() is None):
# TODO(b/326287351): Get string tensor size in tf.function.
total_size = self._dtype_size = 0
if not string_size_warning_printed:
logging.warning("The checkpoint sharding policy is being executed "
"in a tf.function. The size of the string/variant "
"constant cannot be obtained.")
string_size_warning_printed = True
elif self._dtype == dtypes.string:
with ops.device(self._device):
if not context.executing_eagerly():
self._root_tensor = ops.get_default_session().run(
self._root_tensor)
if self._root_shape.rank is None or self._root_shape.rank == 0:
sizes = [string_ops.string_length(self._root_tensor,
unit="BYTE")]
else:
sizes = [string_ops.string_length(elem, unit="BYTE")
for elem in self._root_tensor]
if context.executing_eagerly():
sizes = [size.numpy() for size in sizes]
else:
sizes = ops.get_default_session().run(sizes)
total_size = sum(sizes)
self._dtype_size = max(sizes)
if self._root_shape.rank is None or self._root_shape.rank == 0:
if total_size > self.max_shard_size:
logging.warning(
"Tensor %s is a %s scalar of size %s bytes and cannot be "
"partitioned into a shard of max shard size %s bytes. It will "
"be added as an individual shard that exceeds the max shard "
"size.", self._checkpoint_key, self._dtype, total_size,
self.max_shard_size)
self._large_scalars.append(
{self._checkpoint_key: {self._slice_spec: self._root_tensor}})
else:
if total_size > self._shard_size_remaining:
self._tensors_by_shard.append({})
self._shard_size_remaining = self.max_shard_size
(self._tensors_by_shard[-1]
.setdefault(self._checkpoint_key, {})
[self._slice_spec]) = self._root_tensor
self._shard_size_remaining -= total_size
continue
# Partition tensor and add partitions to shards.
self._working_tensor_offset = self._root_offset[:]
self._working_tensor_shape = self._root_shape
working_tensor_size = total_size
while working_tensor_size > self._shard_size_remaining:
(part_axis, part_size) = self._get_next_partition()
if part_size == 0:
# Tensor partition couldn't fit in remaining shard space. Try again
# with the next full shard.
self._tensors_by_shard.append({})
self._shard_size_remaining = self.max_shard_size
else:
self._add_partition(part_axis, part_size)
working_tensor_size = (
int(math.prod(self._working_tensor_shape)) * self._dtype_size)
if self._working_tensor_shape.num_elements() > 0:
if self._working_tensor_offset and self._working_tensor_shape:
with ops.device(self._device):
working_tensor = array_ops.slice(
self._root_tensor,
begin=list(map(
operator.sub,
self._working_tensor_offset, self._root_offset)),
size=self._working_tensor_shape.as_list())
else:
working_tensor = self._root_tensor
remaining_tensor_slice_spec = variables.Variable.SaveSliceInfo(
full_name=self._checkpoint_key,
full_shape=self._full_shape,
var_offset=self._working_tensor_offset,
var_shape=self._working_tensor_shape).spec.strip()
(self._tensors_by_shard[-1]
.setdefault(self._checkpoint_key, {})
[remaining_tensor_slice_spec]) = working_tensor
self._shard_size_remaining -= working_tensor_size
shards = []
if self._tensors_by_shard[0]:
shards.extend(self._tensors_by_shard)
shards.extend(self._large_scalars)
return shards
def __init__(self, max_shard_size: int):
self.max_shard_size = max_shard_size
@property
def description(self) -> str:
return "Split tensors into shards with a max shard size."
def __call__(
self, shardable_tensors: Sequence[sharding_util.ShardableTensor]
) -> Sequence[sharding_util.Shard]:
return self.MaxShardSizePartitioner().get_shards(
self.max_shard_size, shardable_tensors)