Path: blob/master/site/en-snapshot/guide/function.ipynb
25115 views
Copyright 2020 The TensorFlow Authors.
Better performance with tf.function
In TensorFlow 2, eager execution is turned on by default. The user interface is intuitive and flexible (running one-off operations is much easier and faster), but this can come at the expense of performance and deployability.
You can use tf.function
to make graphs out of your programs. It is a transformation tool that creates Python-independent dataflow graphs out of your Python code. This will help you create performant and portable models, and it is required to use SavedModel
.
This guide will help you conceptualize how tf.function
works under the hood, so you can use it effectively.
The main takeaways and recommendations are:
Debug in eager mode, then decorate with
@tf.function
.Don't rely on Python side effects like object mutation or list appends.
tf.function
works best with TensorFlow ops; NumPy and Python calls are converted to constants.
Setup
Define a helper function to demonstrate the kinds of errors you might encounter:
Basics
Usage
A Function
you define (for example by applying the @tf.function
decorator) is just like a core TensorFlow operation: You can execute it eagerly; you can compute gradients; and so on.
You can use Function
s inside other Function
s.
Function
s can be faster than eager code, especially for graphs with many small ops. But for graphs with a few expensive ops (like convolutions), you may not see much speedup.
Tracing
This section exposes how Function
works under the hood, including implementation details which may change in the future. However, once you understand why and when tracing happens, it's much easier to use tf.function
effectively!
What is "tracing"?
A Function
runs your program in a TensorFlow Graph. However, a tf.Graph
cannot represent all the things that you'd write in an eager TensorFlow program. For instance, Python supports polymorphism, but tf.Graph
requires its inputs to have a specified data type and dimension. Or you may perform side tasks like reading command-line arguments, raising an error, or working with a more complex Python object; none of these things can run in a tf.Graph
.
Function
bridges this gap by separating your code in two stages:
In the first stage, referred to as "tracing",
Function
creates a newtf.Graph
. Python code runs normally, but all TensorFlow operations (like adding two Tensors) are deferred: they are captured by thetf.Graph
and not run.In the second stage, a
tf.Graph
which contains everything that was deferred in the first stage is run. This stage is much faster than the tracing stage.
Depending on its inputs, Function
will not always run the first stage when it is called. See "Rules of tracing" below to get a better sense of how it makes that determination. Skipping the first stage and only executing the second stage is what gives you TensorFlow's high performance.
When Function
does decide to trace, the tracing stage is immediately followed by the second stage, so calling the Function
both creates and runs the tf.Graph
. Later you will see how you can run only the tracing stage with get_concrete_function
.
When you pass arguments of different types into a Function
, both stages are run:
Note that if you repeatedly call a Function
with the same argument type, TensorFlow will skip the tracing stage and reuse a previously traced graph, as the generated graph would be identical.
You can use pretty_printed_concrete_signatures()
to see all of the available traces:
So far, you've seen that tf.function
creates a cached, dynamic dispatch layer over TensorFlow's graph tracing logic. To be more specific about the terminology:
A
tf.Graph
is the raw, language-agnostic, portable representation of a TensorFlow computation.A
ConcreteFunction
wraps atf.Graph
.A
Function
manages a cache ofConcreteFunction
s and picks the right one for your inputs.tf.function
wraps a Python function, returning aFunction
object.Tracing creates a
tf.Graph
and wraps it in aConcreteFunction
, also known as a trace.
Rules of tracing
When called, a Function
matches the call arguments to existing ConcreteFunction
s using tf.types.experimental.TraceType
of each argument. If a matching ConcreteFunction
is found, the call is dispatched to it. If no match is found, a new ConcreteFunction
is traced.
If multiple matches are found, the most specific signature is chosen. Matching is done by subtyping, much like normal function calls in C++ or Java, for instance. For example, TensorShape([1, 2])
is a subtype of TensorShape([None, None])
and so a call to the tf.function with TensorShape([1, 2])
can be dispatched to the ConcreteFunction
produced with TensorShape([None, None])
but if a ConcreteFunction
with TensorShape([1, None])
also exists then it will be prioritized since it is more specific.
The TraceType
is determined from input arguments as follows:
For
Tensor
, the type is parameterized by theTensor
'sdtype
andshape
; ranked shapes are a subtype of unranked shapes; fixed dimensions are a subtype of unknown dimensionsFor
Variable
, the type is similar toTensor
, but also includes a unique resource ID of the variable, necessary to correctly wire control dependenciesFor Python primitive values, the type corresponds to the value itself. For example, the
TraceType
of the value3
isLiteralTraceType<3>
, notint
.For Python ordered containers such as
list
andtuple
, etc., the type is parameterized by the types of their elements; for example, the type of[1, 2]
isListTraceType<LiteralTraceType<1>, LiteralTraceType<2>>
and the type for[2, 1]
isListTraceType<LiteralTraceType<2>, LiteralTraceType<1>>
which is different.For Python mappings such as
dict
, the type is also a mapping from the same keys but to the types of values instead the actual values. For example, the type of{1: 2, 3: 4}
, isMappingTraceType<<KeyValue<1, LiteralTraceType<2>>>, <KeyValue<3, LiteralTraceType<4>>>>
. However, unlike ordered containers,{1: 2, 3: 4}
and{3: 4, 1: 2}
have equivalent types.For Python objects which implement the
__tf_tracing_type__
method, the type is whatever that method returnsFor any other Python objects, the type is a generic
TraceType
, its matching precedure is:First it checks if the object is the same object used in the previous trace (using python
id()
oris
). Note that this will still match if the object has changed, so if you use python objects astf.function
arguments it's best to use immutable ones.Next it checks if the object is equal to the object used in the previous trace (using python
==
).
Note that this procedure only keeps a weakref to the object and hence only works as long as the object is in scope/not deleted.)
Note: TraceType
is based on the Function
input parameters so changes to global and free variables alone will not create a new trace. See this section for recommended practices when dealing with Python global and free variables.
Controlling retracing
Retracing, which is when your Function
creates more than one trace, helps ensure that TensorFlow generates correct graphs for each set of inputs. However, tracing is an expensive operation! If your Function
retraces a new graph for every call, you'll find that your code executes more slowly than if you didn't use tf.function
.
To control the tracing behavior, you can use the following techniques:
Pass a fixed input_signature
to tf.function
Use unknown dimensions for flexibility
Since TensorFlow matches tensors based on their shape, using a None
dimension as a wildcard will allow Function
s to reuse traces for variably-sized input. Variably-sized input can occur if you have sequences of different length, or images of different sizes for each batch (See the Transformer and Deep Dream tutorials for example).
Pass tensors instead of python literals
Often, Python arguments are used to control hyperparameters and graph constructions - for example, num_layers=10
or training=True
or nonlinearity='relu'
. So, if the Python argument changes, it makes sense that you'd have to retrace the graph.
However, it's possible that a Python argument is not being used to control graph construction. In these cases, a change in the Python value can trigger needless retracing. Take, for example, this training loop, which AutoGraph will dynamically unroll. Despite the multiple traces, the generated graph is actually identical, so retracing is unnecessary.
If you need to force retracing, create a new Function
. Separate Function
objects are guaranteed not to share traces.
Use the tracing protocol
Where possible, you should prefer converting the Python type into a tf.experimental.ExtensionType
instead. Moreover, the TraceType
of an ExtensionType
is the tf.TypeSpec
associated with it. Therefore, if needed, you can simply override the default tf.TypeSpec
to take control of an ExtensionType
's Tracing Protocol
. Refer to the Customizing the ExtensionType's TypeSpec section in the Extension types guide for details.
Otherwise, for direct control over when Function
should retrace in regards to a particular Python type, you can implement the Tracing Protocol
for it yourself.
Obtaining concrete functions
Every time a function is traced, a new concrete function is created. You can directly obtain a concrete function, by using get_concrete_function
.
Printing a ConcreteFunction
displays a summary of its input arguments (with types) and its output type.
You can also directly retrieve a concrete function's signature.
Using a concrete trace with incompatible types will throw an error
You may notice that Python arguments are given special treatment in a concrete function's input signature. Prior to TensorFlow 2.3, Python arguments were simply removed from the concrete function's signature. Starting with TensorFlow 2.3, Python arguments remain in the signature, but are constrained to take the value set during tracing.
Obtaining graphs
Each concrete function is a callable wrapper around a tf.Graph
. Although retrieving the actual tf.Graph
object is not something you'll normally need to do, you can obtain it easily from any concrete function.
Debugging
In general, debugging code is easier in eager mode than inside tf.function
. You should ensure that your code executes error-free in eager mode before decorating with tf.function
. To assist in the debugging process, you can call tf.config.run_functions_eagerly(True)
to globally disable and reenable tf.function
.
When tracking down issues that only appear within tf.function
, here are some tips:
Plain old Python
print
calls only execute during tracing, helping you track down when your function gets (re)traced.tf.print
calls will execute every time, and can help you track down intermediate values during execution.tf.debugging.enable_check_numerics
is an easy way to track down where NaNs and Inf are created.pdb
(the Python debugger) can help you understand what's going on during tracing. (Caveat:pdb
will drop you into AutoGraph-transformed source code.)
AutoGraph transformations
AutoGraph is a library that is on by default in tf.function
, and transforms a subset of Python eager code into graph-compatible TensorFlow ops. This includes control flow like if
, for
, while
.
TensorFlow ops like tf.cond
and tf.while_loop
continue to work, but control flow is often easier to write and understand when written in Python.
If you're curious you can inspect the code autograph generates.
Conditionals
AutoGraph will convert some if <condition>
statements into the equivalent tf.cond
calls. This substitution is made if <condition>
is a Tensor. Otherwise, the if
statement is executed as a Python conditional.
A Python conditional executes during tracing, so exactly one branch of the conditional will be added to the graph. Without AutoGraph, this traced graph would be unable to take the alternate branch if there is data-dependent control flow.
tf.cond
traces and adds both branches of the conditional to the graph, dynamically selecting a branch at execution time. Tracing can have unintended side effects; check out AutoGraph tracing effects for more information.
See the reference documentation for additional restrictions on AutoGraph-converted if statements.
Loops
AutoGraph will convert some for
and while
statements into the equivalent TensorFlow looping ops, like tf.while_loop
. If not converted, the for
or while
loop is executed as a Python loop.
This substitution is made in the following situations:
for x in y
: ify
is a Tensor, convert totf.while_loop
. In the special case wherey
is atf.data.Dataset
, a combination oftf.data.Dataset
ops are generated.while <condition>
: if<condition>
is a Tensor, convert totf.while_loop
.
A Python loop executes during tracing, adding additional ops to the tf.Graph
for every iteration of the loop.
A TensorFlow loop traces the body of the loop, and dynamically selects how many iterations to run at execution time. The loop body only appears once in the generated tf.Graph
.
See the reference documentation for additional restrictions on AutoGraph-converted for
and while
statements.
Looping over Python data
A common pitfall is to loop over Python/NumPy data within a tf.function
. This loop will execute during the tracing process, adding a copy of your model to the tf.Graph
for each iteration of the loop.
If you want to wrap the entire training loop in tf.function
, the safest way to do this is to wrap your data as a tf.data.Dataset
so that AutoGraph will dynamically unroll the training loop.
When wrapping Python/NumPy data in a Dataset, be mindful of tf.data.Dataset.from_generator
versus tf.data.Dataset.from_tensor_slices
. The former will keep the data in Python and fetch it via tf.py_function
which can have performance implications, whereas the latter will bundle a copy of the data as one large tf.constant()
node in the graph, which can have memory implications.
Reading data from files via TFRecordDataset
, CsvDataset
, etc. is the most effective way to consume data, as then TensorFlow itself can manage the asynchronous loading and prefetching of data, without having to involve Python. To learn more, see the tf.data
: Build TensorFlow input pipelines guide.
Accumulating values in a loop
A common pattern is to accumulate intermediate values from a loop. Normally, this is accomplished by appending to a Python list or adding entries to a Python dictionary. However, as these are Python side effects, they will not work as expected in a dynamically unrolled loop. Use tf.TensorArray
to accumulate results from a dynamically unrolled loop.
Limitations
TensorFlow Function
has a few limitations by design that you should be aware of when converting a Python function to a Function
.
Executing Python side effects
Side effects, like printing, appending to lists, and mutating globals, can behave unexpectedly inside a Function
, sometimes executing twice or not all. They only happen the first time you call a Function
with a set of inputs. Afterwards, the traced tf.Graph
is reexecuted, without executing the Python code.
The general rule of thumb is to avoid relying on Python side effects in your logic and only use them to debug your traces. Otherwise, TensorFlow APIs like tf.data
, tf.print
, tf.summary
, tf.Variable.assign
, and tf.TensorArray
are the best way to ensure your code will be executed by the TensorFlow runtime with each call.
If you would like to execute Python code during each invocation of a Function
, tf.py_function
is an exit hatch. The drawback of tf.py_function
is that it's not portable or particularly performant, cannot be saved with SavedModel, and does not work well in distributed (multi-GPU, TPU) setups. Also, since tf.py_function
has to be wired into the graph, it casts all inputs/outputs to tensors.
Changing Python global and free variables
Changing Python global and free variables counts as a Python side effect, so it only happens during tracing.
Sometimes unexpected behaviors are very hard to notice. In the example below, the counter
is intended to safeguard the increment of a variable. However because it is a python integer and not a TensorFlow object, it's value is captured during the first trace. When the tf.function
is used, the assign_add
will be recorded unconditionally in the underlying graph. Therefore v
will increase by 1, every time the tf.function
is called. This issue is common among users that try to migrate their Grpah-mode Tensorflow code to Tensorflow 2 using tf.function
decorators, when python side-effects (the counter
in the example) are used to determine what ops to run (assign_add
in the example). Usually, users realize this only after seeing suspicious numerical results, or significantly lower performance than expected (e.g. if the guarded operation is very costly).
A workaround to achieve the expected behavior is using tf.init_scope
to lift the operations outside of the function graph. This ensures that the variable increment is only done once during tracing time. It should be noted init_scope
has other side effects including cleared control flow and gradient tape. Sometimes the usage of init_scope
can become too complex to manage realistically.
In summary, as a rule of thumb, you should avoid mutating python objects such as integers or containers like lists that live outside the Function
. Instead, use arguments and TF objects. For example, the section "Accumulating values in a loop" has one example of how list-like operations can be implemented.
You can, in some cases, capture and manipulate state if it is a tf.Variable
. This is how the weights of Keras models are updated with repeated calls to the same ConcreteFunction
.
Using Python iterators and generators
Many Python features, such as generators and iterators, rely on the Python runtime to keep track of state. In general, while these constructs work as expected in eager mode, they are examples of Python side effects and therefore only happen during tracing.
Just like how TensorFlow has a specialized tf.TensorArray
for list constructs, it has a specialized tf.data.Iterator
for iteration constructs. See the section on AutoGraph transformations for an overview. Also, the tf.data
API can help implement generator patterns:
All outputs of a tf.function must be return values
With the exception of tf.Variable
s, a tf.function must return all its outputs. Attempting to directly access any tensors from a function without going through return values causes "leaks".
For example, the function below "leaks" the tensor a
through the Python global x
:
This is true even if the leaked value is also returned:
Usually, leaks such as these occur when you use Python statements or data structures. In addition to leaking inaccessible tensors, such statements are also likely wrong because they count as Python side effects, and are not guaranteed to execute at every function call.
Common ways to leak local tensors also include mutating an external Python collection, or an object:
Recursive tf.functions are not supported
Recursive Function
s are not supported and could cause infinite loops. For example,
Even if a recursive Function
seems to work, the python function will be traced multiple times and could have performance implication. For example,
Known Issues
If your Function
is not evaluating correctly, the error may be explained by these known issues which are planned to be fixed in the future.
Depending on Python global and free variables
Function
creates a new ConcreteFunction
when called with a new value of a Python argument. However, it does not do that for the Python closure, globals, or nonlocals of that Function
. If their value changes in between calls to the Function
, the Function
will still use the values they had when it was traced. This is different from how regular Python functions work.
For that reason, you should follow a functional programming style that uses arguments instead of closing over outer names.
Another way to update a global value, is to make it a tf.Variable
and use the Variable.assign
method instead.
Depending on Python objects
Passing custom Python objects as arguments to tf.function
is supported but has certain limitations.
For maximum feature coverage, consider transforming the objects into Extension types before passing them to tf.function
. You can also use Python primitives and tf.nest
-compatible structures.
However, as covered in the rules of tracing, when a custom TraceType
is not provided by the custom Python class, tf.function
is forced to use instance-based equality which means it will not create a new trace when you pass the same object with modified attributes.
Using the same Function
to evaluate the modified instance of the model will be buggy since it still has the same instance-based TraceType as the original model.
For that reason, you're recommended to write your Function
to avoid depending on mutable object attributes or implement the Tracing Protocol for the objects to inform Function
about such attributes.
If that is not possible, one workaround is to make new Function
s each time you modify your object to force retracing:
As retracing can be expensive, you can use tf.Variable
s as object attributes, which can be mutated (but not changed, careful!) for a similar effect without needing a retrace.
Creating tf.Variables
Function
only supports singleton tf.Variable
s created once on the first call, and reused across subsequent function calls. The code snippet below would create a new tf.Variable
in every function call, which results in a ValueError
exception.
Example:
A common pattern used to work around this limitation is to start with a Python None value, then conditionally create the tf.Variable
if the value is None:
Using with multiple Keras optimizers
You may encounter ValueError: tf.function only supports singleton tf.Variables created on the first call.
when using more than one Keras optimizer with a tf.function
. This error occurs because optimizers internally create tf.Variables
when they apply gradients for the first time.
If you need to change the optimizer during training, a workaround is to create a new Function
for each optimizer, calling the ConcreteFunction
directly.
Using with multiple Keras models
You may also encounter ValueError: tf.function only supports singleton tf.Variables created on the first call.
when passing different model instances to the same Function
.
This error occurs because Keras models (which do not have their input shape defined) and Keras layers create tf.Variables
s when they are first called. You may be attempting to initialize those variables inside a Function
, which has already been called. To avoid this error, try calling model.build(input_shape)
to initialize all the weights before training the model.
Further reading
To learn about how to export and load a Function
, see the SavedModel guide. To learn more about graph optimizations that are performed after tracing, see the Grappler guide. To learn how to optimize your data pipeline and profile your model, see the Profiler guide.