Lab 03 — TF tf.function Tracing & Graph Analysis
Phase: 01 — PyTorch & TF Framework Internals | Difficulty: ⭐⭐⭐⭐☆ | Time: 3–4 hours
tf.functionis the boundary between eager Python and TensorFlow's graph runtime. Every "the model behaves differently in graph mode" bug lives at this boundary. This lab makes the tracing machinery visible.
What you build
trace_and_inspect— capture theConcreteFunctionfor given inputs and produce aGraphReport: node counts by op type, captured external tensors, structured input signatureRetraceProbe— a wrapper that counts how many times Python tracing actually runs, used to demonstrate (and then prevent) the classic retracing triggersstabilize— apply an explicitinput_signatureso shape/value changes reuse one traceautograph_diff— show the AutoGraph-transformed source of a Python function withif/forcontrol flow (what your Python actually becomes)roundtrip_savedmodel— save → reload → verify outputs match exactlyconvert_tflite_dynamic— TFLite dynamic-range quantization with max-abs output delta vs the TF model on random probe inputs
Key concepts
| Concept | What to understand |
|---|---|
| Trace vs execute | Python body runs ONCE per signature (trace time); the graph runs every call |
| Retracing triggers | New dtype, new rank/shape (without signature), new Python value, new closure object |
| Python side effects | print(), list appends, counters fire at trace time only — a top-3 production bug source |
input_signature | Pins the trace to symbolic shapes — None dims accept any size with zero retraces |
| ConcreteFunction | A traced graph + signature; fn.get_concrete_function(...) exposes the FuncGraph |
| AutoGraph | Rewrites Python if/while into tf.cond/tf.while_loop when operands are tensors |
| TFLite dynamic-range | Weights stored INT8, activations computed FP32 — cheapest quantization, first accuracy checkpoint |
Files
| File | Purpose |
|---|---|
lab.py | Skeleton with # TODO markers — implement each utility |
solution.py | Complete reference implementation with commentary |
test_lab.py | Pytest suite validating retrace counts, graph reports, SavedModel/TFLite round-trips |
requirements.txt | tensorflow>=2.15, numpy, pytest |
Run
pip install -r requirements.txt
pytest test_lab.py -v # validate your lab.py
python solution.py # run the full demo: retrace report + TFLite delta
Suggested TODO order
trace_and_inspect— everything else builds on being able to see aFuncGraphRetraceProbe— prove to yourself exactly which calls retrace and whystabilize— make the retraces stop with aninput_signatureautograph_diff— look at what AutoGraph did to yourifstatementroundtrip_savedmodel, thenconvert_tflite_dynamic
Success criteria
- You can enumerate the retracing triggers from memory and predict the retrace count of a call sequence before running it
- You can explain why a
print()insidetf.functionfires once buttf.printfires every call - Your TFLite dynamic-range conversion shows max output delta < 1e-2 on the small MLP (weights-only quantization on a well-conditioned model is nearly lossless)
Interview Q&A
Q: When does tf.function retrace?
New input dtype; new rank or (without a signature) new concrete shape; a new Python-native
argument value (every distinct Python int/str is a new signature!); a new closure/object
identity for weakly-referenced captures; calling with kwargs vs args inconsistently.
Q: Why did my counter only increment once even though I call the function in a loop?
The increment is a Python side effect — it runs at trace time. The traced graph contains
only TF ops. Use tf.Variable.assign_add to make mutation part of the graph.
Q: Why is passing a Python int as batch_size a performance bug?
Each distinct int is a different trace signature → unbounded retracing. Pass it as a
tf.Tensor or pin an input_signature with tf.TensorSpec([None, ...]).
Q: When is TFLite conversion not lossless?
Dynamic-range quantization perturbs weights (INT8); ops without TFLite kernels get rewritten
(e.g., erf-based GELU → tanh approximation); fused kernels may use different accumulation
order; resource variables get frozen to constants.
Extensions
- Repeat the analysis on BERT-tiny and evaluate the MNLI accuracy delta after dynamic-range quantization (the full-scale version of this lab, see phase README)
- Inspect the XLA HLO with
tf.function(jit_compile=True)andfn.experimental_get_compiler_ir(...)('hlo') - Profile trace time vs execution time with the TF Profiler
References
- tf.function guide — the retracing rules, authoritative
- Introduction to graphs and tf.function
- AutoGraph reference
- TFLite post-training quantization