Avoid GPU Starvation with tf.data: Prefetch and Parallel Map
When training on GPUs, the CPU can become the bottleneck. This guide shows how to use tf.data.map with parallel calls and tf.data.prefetch to keep the GPU busy, with a concrete example, common pitfalls, and how to verify performance.
17 Nov 2025, 17:03 UTC

The Problem: GPU Starvation
When training deep learning models on a GPU, the card can finish processing a batch and then sit idle while the CPU reads, decodes, and prepares the next batch. This idle time is called GPU starvation and reduces overall training throughput.
Why tf.data Helps
The tf.data.Dataset API separates data production (CPU) from data consumption (GPU). By building a pipeline that runs on the CPU and feeding it to the GPU, TensorFlow can overlap work so that the GPU always has a ready batch to process.
Key Transformations
.map()– applies a function to each element; can be parallelized withnum_parallel_calls..batch()– groups elements into tensors of a fixed size..prefetch()– buffers the next batch in host memory while the GPU runs the current one.
Typical Pipeline Order
For maximum throughput, the pipeline should follow:
- Load raw data (e.g., file paths).
.map()withnum_parallel_calls=tf.data.AUTOTUNEto parallelize preprocessing..batch()to form training batches..prefetch(buffer_size=tf.data.AUTOTUNE)as the final step.
Concrete Example
The following script demonstrates a minimal pipeline that reads JPEG files, decodes, resizes, normalizes, batches, and prefetches. Replace file_paths and labels with your own data.
import tensorflow as tf
# Example data – replace with your file paths and labels
file_paths = ["/data/img1.jpg", "/data/img2.jpg", "/data/img3.jpg"]
labels = [0, 1, 0]
# Preprocessing function
def _preprocess(path, label):
# Read the image file
image_bytes = tf.io.read_file(path)
# Decode JPEG to a tensor
image = tf.image.decode_jpeg(image_bytes, channels=3)
# Resize to 224x224
image = tf.image.resize(image, [224, 224])
# Normalize to [0,1]
image = tf.cast(image, tf.float32) / 255.0
return image, label
# Build the dataset
dataset = tf.data.Dataset.from_tensor_slices((file_paths, labels))
# Parallel map – the runtime decides the thread count
dataset = dataset.map(_preprocess, num_parallel_calls=tf.data.AUTOTUNE)
# Batch the data
dataset = dataset.batch(32)
# Prefetch to overlap CPU and GPU
dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)
# Example: iterate once to trigger the pipeline
for batch in dataset.take(1):
images, lbls = batch
print(images.shape, lbls.shape)
Common Pitfalls
Prefetch Before Batch
Placing .prefetch() before .batch() causes the buffer to hold individual elements instead of full batches. This increases the number of buffer entries and can waste memory. Always prefetch after batching.
Python GIL in .map()
If your preprocessing uses a Python library that cannot be expressed as a TensorFlow operation (e.g., custom OpenCV code), you might wrap it with tf.py_function. Because tf.py_function runs in a single Python thread, it is subject to the Global Interpreter Lock (GIL), which limits parallelism regardless of num_parallel_calls. Use it only when necessary and keep the function lightweight.
Excessive Buffer Sizes
Setting a very large value for buffer_size in .prefetch() or .shuffle() can cause host‑side out‑of‑memory errors. tf.data.AUTOTUNE typically selects a safe size, but if you override it, monitor system RAM usage.
How to Verify the Pipeline Works
- Measure Epoch Time: Run training with and without
.prefetch()and compare the total epoch duration. A noticeable reduction indicates effective overlap. - GPU Utilization: Use
nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits -l 1to watch GPU usage every second. Persistent high usage (>80%) between batches suggests the GPU is not starved. - CPU Utilization: Run
htoportopduring training. If multiple CPU cores are consistently busy (e.g., >70% usage) while the GPU is active, the pipeline is likely keeping the GPU fed. - Buffer Size Check: In Python, you can inspect the prefetch buffer size by calling
dataset.element_specand printing the shape; however, the actual buffer size is internal. Usetf.data.experimental.get_stats(dataset)in TensorFlow 2.6+ to see buffer statistics if available.
When diagnosing, look for the following signs of starvation:
- GPU utilization drops to near zero between steps.
- Only one CPU core is heavily used while others remain idle.
- Training time is significantly longer than expected for the dataset size.
Adjusting num_parallel_calls and prefetch buffer size, or restructuring the pipeline, usually resolves these issues. Always validate changes with a small training run before scaling to full training.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.