tensorflow py_func is handy but makes my training step very slow.
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
tf.py_func (TF1) and tf.py_function (TF2) let you run arbitrary Python code inside a TensorFlow computation graph. They are convenient for custom preprocessing, metrics, or operations that lack TensorFlow equivalents. However, they force execution back to Python on the CPU, bypassing TF's graph optimizer, XLA compilation, and GPU acceleration. A single py_func in your training loop can drop throughput by 5-50x because it serializes execution through the Python GIL.
Why py_func Is Slow
Each call to py_function:
- Copies tensor data from GPU to CPU (device-to-host transfer)
- Acquires the Python GIL (blocks all other Python threads)
- Runs the Python function (not optimized, no vectorization)
- Copies the result back to GPU (host-to-device transfer)
- Breaks graph optimization (XLA cannot see through py_func)
The GPU sits idle waiting for the Python function to finish.
Measuring the Impact
Fix 1: Replace with TensorFlow Ops
Most Python/NumPy operations have TensorFlow equivalents:
Common replacements:
| NumPy / Python | TensorFlow Equivalent |
np.clip | tf.clip_by_value |
np.where | tf.where |
np.concatenate | tf.concat |
np.reshape | tf.reshape |
np.argmax | tf.argmax |
np.random.shuffle | tf.random.shuffle |
cv2.resize | tf.image.resize |
scipy.ndimage.rotate | tf.image.rot90 / tfa.image.rotate |
Fix 2: Move py_func to Data Pipeline
If you must use py_func, isolate it in the data pipeline with prefetching:
prefetch(AUTOTUNE) runs the Python preprocessing on CPU while the GPU trains on the previous batch. num_parallel_calls=AUTOTUNE runs multiple py_function calls in parallel (limited by GIL for CPU-bound work, but helps for I/O-bound operations).
Fix 3: Use tf.numpy_function (TF2)
tf.numpy_function is the TF2 replacement that gives cleaner semantics:
Still slow for the same reasons, but avoids the .numpy() conversion step.
Fix 4: Custom TensorFlow Op
For operations called millions of times, write a custom C++/CUDA op:
This runs on GPU with full graph optimization. Only worth the effort for ops called in every training step on every batch.
Fix 5: Use TensorFlow Addons or tf.image
Many custom operations already exist in TensorFlow or its ecosystem:
Fix 6: Process Offline
If the custom operation does not depend on training state, preprocess the entire dataset once and save:
Common Pitfalls
- py_func inside @tf.function:
tf.py_functionworks inside@tf.functionbut disables graph optimization for that portion. The entire traced function may fall back to eager mode. - Unknown output shapes: After
py_function, TensorFlow does not know the output shape. Callresult.set_shape(...)explicitly, or downstream operations that need static shapes will fail. - GIL bottleneck:
num_parallel_callshelps with I/O-bound Python functions but not CPU-bound ones. Python's GIL serializes CPU-intensive numpy operations across threads. - Memory copies: Each
py_functioncall copies data between TensorFlow's memory and Python's memory. For large tensors (images, embeddings), this copy alone can be the bottleneck. - Using py_func for simple math: Operations like clipping, normalization, and type casting all have direct TF equivalents. Check
tf.math,tf.image, andtf.signalbefore usingpy_function.
Summary
tf.py_func/tf.py_functionforces execution back to Python CPU, bypassing GPU and graph optimization- Replace with native TensorFlow ops (
tf.clip_by_value,tf.image.*,tf.math.*) whenever possible - If unavoidable, isolate
py_functionin the data pipeline withprefetch(AUTOTUNE)to overlap with GPU training - Set output shapes explicitly with
set_shape()afterpy_function - For dataset-wide preprocessing, process offline and save to TFRecord
- Check TensorFlow Addons (
tfa) for advanced operations before writing custom Python functions

