410 lines
16 KiB
Python
410 lines
16 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.
|
|
# ==============================================================================
|
|
"""Cond function for Control Flow Operations."""
|
|
|
|
from tensorflow.python.eager import context
|
|
from tensorflow.python.eager.polymorphic_function import eager_function_run
|
|
from tensorflow.python.framework import dtypes
|
|
from tensorflow.python.framework import indexed_slices
|
|
from tensorflow.python.framework import ops
|
|
from tensorflow.python.framework import tensor as tensor_lib
|
|
from tensorflow.python.framework import tensor_util
|
|
from tensorflow.python.ops import array_ops
|
|
from tensorflow.python.ops import cond_v2
|
|
from tensorflow.python.ops import control_flow_ops
|
|
from tensorflow.python.ops import control_flow_util as util
|
|
from tensorflow.python.ops import math_ops
|
|
from tensorflow.python.platform import tf_logging as logging
|
|
from tensorflow.python.types import core
|
|
from tensorflow.python.util import deprecation
|
|
from tensorflow.python.util import dispatch
|
|
from tensorflow.python.util import nest
|
|
from tensorflow.python.util.tf_export import tf_export
|
|
|
|
|
|
# pylint: disable=redefined-outer-name
|
|
# pylint: disable=g-doc-args
|
|
@tf_export(v1=["cond"])
|
|
@dispatch.add_dispatch_support
|
|
@deprecation.deprecated_args(
|
|
None, "fn1/fn2 are deprecated in favor of the true_fn/false_fn arguments.",
|
|
"fn1", "fn2")
|
|
def cond(pred,
|
|
true_fn=None,
|
|
false_fn=None,
|
|
strict=False,
|
|
name=None,
|
|
fn1=None,
|
|
fn2=None):
|
|
"""Return `true_fn()` if the predicate `pred` is true else `false_fn()`.
|
|
|
|
`true_fn` and `false_fn` both return lists of output tensors. `true_fn` and
|
|
`false_fn` must have the same non-zero number and type of outputs.
|
|
|
|
**WARNING**: Any Tensors or Operations created outside of `true_fn` and
|
|
`false_fn` will be executed regardless of which branch is selected at runtime.
|
|
|
|
Although this behavior is consistent with the dataflow model of TensorFlow,
|
|
it has frequently surprised users who expected a lazier semantics.
|
|
Consider the following simple program:
|
|
|
|
```python
|
|
z = tf.multiply(a, b)
|
|
result = tf.cond(x < y, lambda: tf.add(x, z), lambda: tf.square(y))
|
|
```
|
|
|
|
If `x < y`, the `tf.add` operation will be executed and `tf.square`
|
|
operation will not be executed. Since `z` is needed for at least one
|
|
branch of the `cond`, the `tf.multiply` operation is always executed,
|
|
unconditionally.
|
|
|
|
Note that `cond` calls `true_fn` and `false_fn` *exactly once* (inside the
|
|
call to `cond`, and not at all during `Session.run()`). `cond`
|
|
stitches together the graph fragments created during the `true_fn` and
|
|
`false_fn` calls with some additional graph nodes to ensure that the right
|
|
branch gets executed depending on the value of `pred`.
|
|
|
|
`tf.cond` supports nested structures as implemented in
|
|
`tensorflow.python.util.nest`. Both `true_fn` and `false_fn` must return the
|
|
same (possibly nested) value structure of lists, tuples, and/or named tuples.
|
|
Singleton lists and tuples form the only exceptions to this: when returned by
|
|
`true_fn` and/or `false_fn`, they are implicitly unpacked to single values.
|
|
This behavior is disabled by passing `strict=True`.
|
|
|
|
Args:
|
|
pred: A scalar determining whether to return the result of `true_fn` or
|
|
`false_fn`.
|
|
true_fn: The callable to be performed if pred is true.
|
|
false_fn: The callable to be performed if pred is false.
|
|
strict: A boolean that enables/disables 'strict' mode; see above.
|
|
name: Optional name prefix for the returned tensors.
|
|
|
|
Returns:
|
|
Tensors returned by the call to either `true_fn` or `false_fn`. If the
|
|
callables return a singleton list, the element is extracted from the list.
|
|
|
|
Raises:
|
|
TypeError: if `true_fn` or `false_fn` is not callable.
|
|
ValueError: if `true_fn` and `false_fn` do not return the same number of
|
|
tensors, or return tensors of different types.
|
|
|
|
Example:
|
|
|
|
```python
|
|
x = tf.constant(2)
|
|
y = tf.constant(5)
|
|
def f1(): return tf.multiply(x, 17)
|
|
def f2(): return tf.add(y, 23)
|
|
r = tf.cond(tf.less(x, y), f1, f2)
|
|
# r is set to f1().
|
|
# Operations in f2 (e.g., tf.add) are not executed.
|
|
```
|
|
|
|
"""
|
|
# We needed to make true_fn/false_fn keyword arguments for
|
|
# backwards-compatibility. This check exists so that we can convert back to
|
|
# having them be positional arguments.
|
|
# TODO(josh11b): Make `true_fn` and `false_fn` positional arguments after
|
|
# `fn1` and `fn2` are deleted.
|
|
if fn1 is not None:
|
|
if true_fn is not None:
|
|
raise TypeError(
|
|
"cond(): 'true_fn' and 'fn1' may not be set simultaneously.")
|
|
true_fn = fn1
|
|
elif true_fn is None:
|
|
raise TypeError("cond(): 'true_fn' argument required")
|
|
if fn2 is not None:
|
|
if false_fn is not None:
|
|
raise TypeError(
|
|
"cond(): 'false_fn' and 'fn2' may not be set simultaneously.")
|
|
false_fn = fn2
|
|
elif false_fn is None:
|
|
raise TypeError("cond(): 'false_fn' argument required")
|
|
|
|
if not callable(true_fn):
|
|
raise TypeError("'true_fn' must be callable.")
|
|
if not callable(false_fn):
|
|
raise TypeError("'false_fn' must be callable.")
|
|
|
|
if context.executing_eagerly():
|
|
return _eager_cond_implementation(pred, true_fn, false_fn, strict, name)
|
|
|
|
# Always enable control flow v2 if building a function, regardless of toggle.
|
|
if util.EnableControlFlowV2(ops.get_default_graph()):
|
|
return cond_v2.cond_v2(pred, true_fn, false_fn, name)
|
|
|
|
with ops.name_scope(name, "cond", [pred]):
|
|
# Add the Switch to the graph.
|
|
if isinstance(pred, bool):
|
|
raise TypeError("'pred' must not be a Python bool.")
|
|
p_2, p_1 = control_flow_ops.switch(pred, pred)
|
|
pivot_1 = array_ops.identity(p_1, name="switch_t")
|
|
pivot_2 = array_ops.identity(p_2, name="switch_f")
|
|
pred = array_ops.identity(pred, name="pred_id")
|
|
# Disable the fetching of tensors that are only on one branch of cond.
|
|
for tensor in [p_1, p_2, pivot_1, pivot_2, pred]:
|
|
tensor.op.graph.prevent_fetching(tensor.op)
|
|
|
|
# Build the graph for the true branch in a new context.
|
|
context_t = control_flow_ops.CondContext(pred, pivot_1, branch=1)
|
|
try:
|
|
context_t.Enter()
|
|
orig_res_t, res_t = context_t.BuildCondBranch(true_fn)
|
|
if orig_res_t is None:
|
|
raise ValueError("'true_fn' must have a return value.")
|
|
context_t.ExitResult(res_t)
|
|
finally:
|
|
context_t.Exit()
|
|
|
|
# Build the graph for the false branch in a new context.
|
|
context_f = control_flow_ops.CondContext(pred, pivot_2, branch=0)
|
|
try:
|
|
context_f.Enter()
|
|
orig_res_f, res_f = context_f.BuildCondBranch(false_fn)
|
|
if orig_res_f is None:
|
|
raise ValueError("'false_fn' must have a return value.")
|
|
context_f.ExitResult(res_f)
|
|
finally:
|
|
context_f.Exit()
|
|
|
|
if not strict:
|
|
orig_res_t = _UnpackIfSingleton(orig_res_t)
|
|
orig_res_f = _UnpackIfSingleton(orig_res_f)
|
|
|
|
# Check that the return values of the two branches have the same structure.
|
|
try:
|
|
nest.assert_same_structure(orig_res_t, orig_res_f, expand_composites=True)
|
|
except (TypeError, ValueError):
|
|
nest.map_structure(_cast_indexed_slice_indices, orig_res_t, orig_res_f)
|
|
nest.map_structure(_cast_indexed_slice_indices, res_t, res_f)
|
|
try:
|
|
nest.assert_same_structure(orig_res_t, orig_res_f,
|
|
expand_composites=True)
|
|
except TypeError as e:
|
|
raise TypeError(
|
|
f"Incompatible return types of 'true_fn' and 'false_fn': {e}")
|
|
except ValueError as e:
|
|
raise ValueError(
|
|
f"Incompatible return values of 'true_fn' and 'false_fn': {e}")
|
|
|
|
# Add the final merge to the graph.
|
|
if not res_t:
|
|
raise ValueError(
|
|
"'true_fn' and 'false_fn' must return at least one result.")
|
|
|
|
res_t_flat = nest.flatten(res_t, expand_composites=True)
|
|
res_f_flat = nest.flatten(res_f, expand_composites=True)
|
|
|
|
for (x, y) in zip(res_t_flat, res_f_flat):
|
|
assert (
|
|
isinstance(x, tensor_lib.Tensor)
|
|
and isinstance(y, tensor_lib.Tensor))
|
|
if x.dtype.base_dtype != y.dtype.base_dtype:
|
|
raise ValueError(
|
|
"Outputs of 'true_fn' and 'false_fn' must have the same type(s). "
|
|
f"Received {x.dtype.name} from 'true_fn' "
|
|
f"and {y.dtype.name} from 'false_fn'.")
|
|
|
|
merges = [
|
|
control_flow_ops.merge(pair)[0] for pair in zip(res_f_flat, res_t_flat)]
|
|
merges = nest.map_structure(
|
|
control_flow_ops._convert_flow_to_tensorarray, # pylint: disable=protected-access
|
|
nest.flatten(orig_res_t, expand_composites=True),
|
|
merges)
|
|
|
|
# Only add non-nested conds to the collection. Any nested control flow will
|
|
# be encapsulated in the root context.
|
|
assert context_t.outer_context == context_f.outer_context
|
|
if context_t.outer_context is None:
|
|
ops.add_to_collection(ops.GraphKeys.COND_CONTEXT, context_t)
|
|
ops.add_to_collection(ops.GraphKeys.COND_CONTEXT, context_f)
|
|
|
|
merges = nest.pack_sequence_as(
|
|
structure=orig_res_t, flat_sequence=merges, expand_composites=True)
|
|
|
|
# Singleton lists and tuples are automatically unpacked if strict == False.
|
|
if not strict:
|
|
merges = _UnpackIfSingleton(merges)
|
|
return merges
|
|
|
|
|
|
@tf_export("cond", v1=[])
|
|
@dispatch.add_dispatch_support
|
|
def cond_for_tf_v2(pred, true_fn=None, false_fn=None, name=None):
|
|
"""Return `true_fn()` if the predicate `pred` is true else `false_fn()`.
|
|
|
|
Note: This op is automatically used in a `tf.function` to convert Python
|
|
if-statements when the predicate is a `tf.Tensor`, unless `autograph=False` is
|
|
explicitly specified in `tf.function` args. For example, the following are
|
|
equivalent:
|
|
|
|
>>> @tf.function
|
|
... def fun1(x,y):
|
|
... if x > 0: # AutoGraph converts if-statement to tf.cond().
|
|
... z = y+1
|
|
... else:
|
|
... z = y-1
|
|
... return z
|
|
>>> print(fun1(tf.constant(7), tf.constant(3)).numpy())
|
|
4
|
|
|
|
>>> @tf.function
|
|
... def fun2(x,y):
|
|
... pred = x > 0
|
|
... true_fn = lambda: y+1
|
|
... false_fn = lambda: y-1
|
|
... return tf.cond(pred, true_fn, false_fn) # Use tf.cond() explicitly.
|
|
>>> print(fun1(tf.constant(7), tf.constant(3)).numpy())
|
|
4
|
|
|
|
For more information, see [tf.function and AutoGraph guide](
|
|
https://www.tensorflow.org/guide/function#autograph_transformations).
|
|
|
|
`true_fn` and `false_fn` both return lists of output tensors. `true_fn` and
|
|
`false_fn` must have the same non-zero number and type of outputs.
|
|
|
|
**WARNING**: Any Tensors or Operations created outside of `true_fn` and
|
|
`false_fn` will be executed regardless of which branch is selected at runtime.
|
|
|
|
Although this behavior is consistent with the dataflow model of TensorFlow,
|
|
it has frequently surprised users who expected a lazier semantics.
|
|
Consider the following simple program:
|
|
|
|
>>> x, y = tf.constant(2, dtype=tf.int32), tf.constant(4, dtype=tf.int32)
|
|
>>> z = tf.multiply(x, y)
|
|
>>> r = tf.cond(x < y, lambda: tf.add(x, z), lambda: tf.square(y))
|
|
>>> print(r.numpy())
|
|
10
|
|
|
|
If `x < y`, the `tf.add` operation will be executed and `tf.square`
|
|
operation will not be executed. Since `z` is needed for at least one
|
|
branch of the `cond`, the `tf.multiply` operation is always executed,
|
|
unconditionally.
|
|
|
|
Note that `cond` calls `true_fn` and `false_fn` *exactly once* (inside the
|
|
call to `cond`, and not at all during `Session.run()`). `cond`
|
|
stitches together the graph fragments created during the `true_fn` and
|
|
`false_fn` calls with some additional graph nodes to ensure that the right
|
|
branch gets executed depending on the value of `pred`.
|
|
|
|
`tf.cond` supports nested structures as implemented in
|
|
`tensorflow.python.util.nest`. Both `true_fn` and `false_fn` must return the
|
|
same (possibly nested) value structure of lists, tuples, and/or named tuples.
|
|
Singleton lists and tuples form the only exceptions to this: when returned by
|
|
`true_fn` and/or `false_fn`, they are implicitly unpacked to single values.
|
|
|
|
Note: It is illegal to "directly" use tensors created inside a cond branch
|
|
outside it, e.g. by storing a reference to a branch tensor in the python
|
|
state. If you need to use a tensor created in a branch function you should
|
|
return it as an output of the branch function and use the output from
|
|
`tf.cond` instead.
|
|
|
|
Args:
|
|
pred: A scalar determining whether to return the result of `true_fn` or
|
|
`false_fn`.
|
|
true_fn: The callable to be performed if pred is true.
|
|
false_fn: The callable to be performed if pred is false.
|
|
name: Optional name prefix for the returned tensors.
|
|
|
|
Returns:
|
|
Tensors returned by the call to either `true_fn` or `false_fn`. If the
|
|
callables return a singleton list, the element is extracted from the list.
|
|
|
|
Raises:
|
|
TypeError: if `true_fn` or `false_fn` is not callable.
|
|
ValueError: if `true_fn` and `false_fn` do not return the same number of
|
|
tensors, or return tensors of different types.
|
|
|
|
Example:
|
|
|
|
>>> x = tf.constant(2)
|
|
>>> y = tf.constant(5)
|
|
>>> def f1(): return tf.multiply(x, 7)
|
|
>>> def f2(): return tf.add(y, 3)
|
|
>>> r = tf.cond(tf.less(x, y), f1, f2)
|
|
>>> # r is set to f1().
|
|
>>> # Operations in f2 (e.g., tf.add) are not executed.
|
|
>>> print(r.numpy())
|
|
14
|
|
|
|
"""
|
|
return cond(pred, true_fn=true_fn, false_fn=false_fn, strict=True, name=name)
|
|
|
|
|
|
def _UnpackIfSingleton(res):
|
|
if isinstance(res, (list, tuple)) and len(res) == 1:
|
|
return res[0]
|
|
else:
|
|
return res
|
|
|
|
|
|
def _eager_cond_implementation(pred, true_fn, false_fn, strict, name):
|
|
"""Special cases for `cond` when executing eagerly."""
|
|
pred = ops.convert_to_tensor(pred)
|
|
pred_constant_value = tensor_util.constant_value(pred)
|
|
if pred_constant_value is None:
|
|
# Eager tensors from a parallel device may not have a constant
|
|
# value. Running the cond op itself would work, but we don't have logic to
|
|
# build cond ops without wrapping in a function first.
|
|
if (not isinstance(true_fn, core.PolymorphicFunction)
|
|
or not isinstance(false_fn, core.PolymorphicFunction)):
|
|
raise TypeError("When running tf.cond on a parallel device, 'true_fn' "
|
|
"and 'false_fn' must be decorated with `tf.function`.")
|
|
functions_run_eagerly = eager_function_run.functions_run_eagerly()
|
|
if functions_run_eagerly:
|
|
# We need to use tf.function to deal with variable creation inside the
|
|
# cond, and skipping it because of run_functions_eagerly would just
|
|
# crash immediately.
|
|
logging.warning(
|
|
"It looks like tf.function behavior was disabled, perhaps using "
|
|
"tf.config.run_functions_eagerly. Parallelized tf.cond requires "
|
|
"tf.function to work. This primitive will override the disable.")
|
|
eager_function_run.run_functions_eagerly(False)
|
|
try:
|
|
return cond_v2.cond_v2(pred, true_fn, false_fn, name)
|
|
finally:
|
|
if functions_run_eagerly is not None:
|
|
eager_function_run.run_functions_eagerly(functions_run_eagerly)
|
|
else:
|
|
# For conditions which are eager tensors with a constant value (most of
|
|
# them), we only call the relevant branch function and execute it eagerly.
|
|
with ops.name_scope(name, "cond", [pred]):
|
|
if pred_constant_value:
|
|
result = true_fn()
|
|
else:
|
|
result = false_fn()
|
|
if not strict:
|
|
result = _UnpackIfSingleton(result)
|
|
return result
|
|
|
|
|
|
def _cast_indexed_slice_indices(a, b):
|
|
"""Cast IndexedSlice.indices from int32 to int64 where necessary.
|
|
|
|
If `a` and `b` are both IndexedSlices, and their indices have different
|
|
dtypes, then cast both their dtypes to `int64` (modifies `a` and `b`
|
|
in-place). Otherwise, does nothing.
|
|
|
|
Args:
|
|
a: A value, which may be an IndexedSlices.
|
|
b: A value, which may be an IndexedSlices.
|
|
"""
|
|
if (isinstance(a, indexed_slices.IndexedSlices) and
|
|
isinstance(b, indexed_slices.IndexedSlices) and
|
|
a.indices.dtype != b.indices.dtype):
|
|
# pylint: disable=protected-access
|
|
a._indices = math_ops.cast(a.indices, dtypes.int64)
|
|
b._indices = math_ops.cast(b.indices, dtypes.int64)
|