What is the difference between eager execution and graph execution in TensorFlow 2.x, and what does @tf.function actually do?
Quick Answer
TF2 runs eagerly by default — ops execute immediately like NumPy. @tf.function traces the Python function into a static graph that gets optimized (fusion, pruning, parallelism) and reused, giving big speedups but introducing tracing semantics you must understand.
Detailed Answer
Eager mode is imperative: each op runs as Python executes, easy to debug with print/pdb. @tf.function converts the function into a tf.Graph via tracing: the first call executes the Python once, records TF ops into a graph, and later calls run the compiled graph without touching your Python (a 'concrete function' per input signature). Consequences interviewers probe: Python side effects (print, list.append) run only during tracing, not per call; every new input shape/dtype (or new Python-object argument) triggers a retrace — a common performance bug with variable shapes unless you pass input_signature or use dynamic dimensions; and control flow on tensor values compiles to tf.cond/tf.while via AutoGraph. Graph mode is what enables serialization to SavedModel and deployment to TF Serving/TFLite, so production paths are effectively always graph mode.
Code Example
@tf.function(input_signature=[tf.TensorSpec([None, 224, 224, 3], tf.float32)])
def serve(x):
return model(x, training=False)
print(serve.pretty_printed_concrete_signatures()) # inspect tracesInterview Tip
The retracing trap is the discriminator question. Say 'a new Python argument value causes a retrace, a new tensor value does not' and explain input_signature — that's the difference between having read the docs and having debugged it.