Tensor Flow 📂 Graphs and Speed with tf.function · 2 of 5 28 min read

@tf.function and Tracing Explained: Concrete Functions and AutoGraph

Understand what happens when you call a tf.function: signature check, tracing with symbolic tensors, and a cache of concrete functions. See print vs tf.print, inspect the cache, and learn how AutoGraph turns if and for into tf.cond and tf.while_loop. Play with a trace cache simulator and a loop unroller.

Section 01

The Story — The Tailor's Paper Pattern

Measure Once, Cut Many Times
A tailor gets a new customer. He takes careful measurements and draws a paper pattern. That is slow work.

The next customer has exactly the same size. The tailor does not measure again. He picks up the same pattern and starts cutting at once.

A customer with a different size arrives. Now the tailor must draw a new pattern and add it to his drawer.

@tf.function works the same way. Drawing the pattern is called tracing. The drawer of patterns is a cache of graphs. The "size" of the customer is the input signature.

Section 02

Two Ways to Use tf.function

import tensorflow as tf

@tf.function                      # 1. as a decorator
def scale(x):
    return x * 10

def shift(x):
    return x + 1
fast_shift = tf.function(shift)   # 2. wrap an existing function

print(scale(tf.constant([1.0, 2.0])))
print(fast_shift(tf.constant([1.0, 2.0])))
OUTPUT
tf.Tensor([10. 20.], shape=(2,), dtype=float32) tf.Tensor([2. 3.], shape=(2,), dtype=float32)

Both give a polymorphic function: one Python name that can hold many graphs, one per input signature.


Section 03

What Tracing Really Does

01
Check the signature
TensorFlow looks at the arguments: shape and dtype of each tensor, and the exact value of each Python object.
02
Cache hit? Run the graph
If a graph for this signature already exists, run it. No Python code runs at all.
03
Cache miss? Trace
Run your Python function once with symbolic tensors (shape and dtype, but no values). Every TF op is recorded as a node.
04
Store the concrete function
The finished graph is saved in the cache, then run with the real values.

You can see tracing happen. Python's print runs only during tracing, and it sees a symbolic tensor. tf.print is a graph op, so it runs on every call with real values.

@tf.function
def double(x):
    print("  [trace] Python sees:", x)
    tf.print("  [run]   value is", x)
    return x * 2

print("call 1"); double(tf.constant(5))
print("call 2"); double(tf.constant(7))
print("call 3"); double(tf.constant(1.5))
print("traces:", double.experimental_get_tracing_count())
OUTPUT
call 1 [trace] Python sees: Tensor("x:0", shape=(), dtype=int32) [run] value is 5 call 2 [run] value is 7 call 3 [trace] Python sees: Tensor("x:0", shape=(), dtype=float32) [run] value is 1.5 traces: 2

Look Inside the Cache

print(double.pretty_printed_concrete_signatures())
OUTPUT
Input Parameters: x (POSITIONAL_OR_KEYWORD): TensorSpec(shape=(), dtype=tf.int32, name=None) Output Type: TensorSpec(shape=(), dtype=tf.int32, name=None) Captures: None Input Parameters: x (POSITIONAL_OR_KEYWORD): TensorSpec(shape=(), dtype=tf.float32, name=None) Output Type: TensorSpec(shape=(), dtype=tf.float32, name=None) Captures: None
🧾
Polymorphic vs Concrete

double is a polymorphic function. It holds two concrete functions: one for int32 scalars and one for float32 scalars. Get one directly with double.get_concrete_function(tf.TensorSpec([], tf.float32)).


Section 04

Try It — The Trace Cache Simulator

🗂️ Will This Call Draw a New Pattern? Interactive

Build an argument for f(x) and press Call. Watch the cache fill up. Turn on reduce_retracing and call with tensors of length 1, 2, 3 to see TensorFlow relax the shape to None.

#Concrete function signatureTimes used

Section 05

AutoGraph — Python Control Flow Becomes Graph Ops

During tracing, a helper called AutoGraph rewrites your if, for and while statements. What it produces depends on one question: is the condition a tensor, or a Python value?

Your codeCondition is a…In the graph
if x > 0:Tensortf.cond — both branches are stored, one runs
if flag:Python boolBranch chosen at trace time. The other is never in the graph.
for i in tf.range(n):Tensortf.while_loop — one small loop node
for i in range(n):Python intUnrolled — the body is copied n times
while x < 10:Tensortf.while_loop
@tf.function
def sign_flip(x):
    if x > 0:                 # tensor condition -> tf.cond
        return x
    return -x

ops = [op.type for op in sign_flip.get_concrete_function(tf.constant(1)).graph.get_operations()]
print("tensor if     :", ops)

@tf.function
def add_python_loop(x):
    for _ in range(100):      # Python loop -> unrolled
        x = x + 1
    return x

@tf.function
def add_tf_loop(x):
    for _ in tf.range(100):   # tensor loop -> tf.while_loop
        x = x + 1
    return x

for fn in (add_python_loop, add_tf_loop):
    g = fn.get_concrete_function(tf.constant(0)).graph
    print(f"{fn.__name__:16s}: {len(g.get_operations()):3d} ops, result {fn(tf.constant(0)).numpy()}")
OUTPUT
tensor if : ['Placeholder', 'Const', 'Greater', 'StatelessIf', 'Identity', 'Identity', 'Identity'] add_python_loop : 202 ops, result 100 add_tf_loop : 18 ops, result 100
🐌
Unrolled Loops Make Huge Graphs

A Python range(10000) loop inside a tf.function copies the body ten thousand times. Tracing becomes very slow and memory grows. Loop over tf.range (or a tf.data.Dataset) when the loop is long.


Section 06

Try It — The Loop Unroller

🔄 Python Loop or Tensor Loop? Interactive

Choose the loop type and the number of steps. See how the traced graph grows. The op counts follow the real numbers from the code above (2n + 2 for an unrolled add loop).


Section 07

Golden Rules

✂️ tf.function and Tracing — Rules to Remember
1
The first call with a new signature traces: Python runs once and a graph is stored. Later matching calls skip Python.
2
A tensor's signature is its shape and dtype. A Python value's signature is its exact value.
3
print runs at trace time only. tf.print runs on every call.
4
Conditions on tensors become tf.cond / tf.while_loop. Conditions on Python values are decided once, at trace time.
5
Use tf.range for long loops inside a tf.function to avoid giant unrolled graphs.