Concrete functions and graphs

TensorFlow · Expert · 14 min

This is the machinery behind SavedModel signatures, retracing and graph-level performance tuning.

A tf.function is polymorphic: it keeps a cache of ConcreteFunctions, one per traced input signature. A ConcreteFunction wraps a single FuncGraph (a tf.Graph) with fixed input specs; calling it never retraces, and an incompatible input is an error.

Get one with f.get_concrete_function(tf.TensorSpec([None, 3], tf.float32), ...). From there you can inspect:

  • cf.structured_input_signature and cf.structured_outputs.
  • cf.graph.get_operations(): the ops (Placeholder, MatMul, AddV2, Identity...) and their tensors.
  • cf.graph.as_graph_def(): the protobuf serialised into SavedModels.

SavedModel signatures are concrete functions, which is why serving requires fixed input specs.

Before execution, the runtime passes graphs through Grappler, TensorFlow's graph optimiser: constant folding, arithmetic simplification, common-subexpression elimination, layout optimisation (NHWC/NCHW), remapping op patterns to fused kernels, and pruning of unused ops. You can toggle passes with tf.config.optimizer.set_experimental_options({...}), mainly for debugging.

Graph execution also runs independent ops in parallel and avoids Python overhead per op, which is where most of tf.function's speedup over eager mode comes from in models made of many small operations.

Example

import tensorflow as tf

@tf.function
def f(x, y):
    return tf.matmul(x, y) + 1.0

cf = f.get_concrete_function(tf.TensorSpec([None, 3], tf.float32),
                             tf.TensorSpec([3, 2], tf.float32))
print(cf.structured_input_signature[0])
# (TensorSpec(shape=(None, 3), ...), TensorSpec(shape=(3, 2), ...))
print(cf.outputs[0].shape)                     # (None, 2)
print([op.type for op in cf.graph.get_operations()])
# e.g. ['Placeholder', 'Placeholder', 'MatMul', 'Const', 'AddV2', 'Identity']

print(cf(tf.ones([4, 3]), tf.ones([3, 2])).numpy()[0])   # [4. 4.]
try:
    cf(tf.ones([4, 5]), tf.ones([5, 2]))       # concrete functions never retrace
except Exception as e:
    print("incompatible input:", type(e).__name__)
f(tf.ones([4, 5]), tf.ones([5, 2]))            # the polymorphic f simply retraces

Remember

  • A tf.function caches one ConcreteFunction per input signature
  • ConcreteFunctions expose their tf.Graph and never retrace
  • Grappler rewrites graphs (folding, fusion, layout) before running them

Watch out for

  • Expecting a ConcreteFunction to accept new shapes; only the polymorphic tf.function retraces.

Check yourself

  1. What is a SavedModel serving signature, in terms of tf.function machinery?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.tensorflow.org/guide/intro_to_graphs