Tensor Flow 30 min read

tf.data map, shuffle, batch, cache and prefetch Explained

Build fast input pipelines with map, filter, shuffle, batch, cache and prefetch. Learn why the shuffle buffer size matters, the right order of steps, and how num_parallel_calls and AUTOTUNE hide waiting time. A real benchmark shows the speed-up. Play with the shuffle buffer simulator and the pipeline timeline builder.

Section 01

The Story — A Restaurant Kitchen at Rush Hour

Prep, Mix, Plate, and Never Keep the Guest Waiting
A busy kitchen has stations. One cook prepares each ingredient (map). Orders are mixed up so no table gets only soup (shuffle). Dishes go out on trays of four (batch). Sauces made once are kept in the fridge for tomorrow (cache).

The best trick: while the waiter carries one tray out, the next tray is already being plated (prefetch). The guest — your GPU — never waits.

A tf.data pipeline is a chain of these stations. Each method returns a new dataset, so you build the pipeline step by step. The order of the steps changes both what you get and how fast you get it.

MethodWhat it doesTypical call
map(f)Apply a function to every element (decode, resize, normalise, augment)map(f, num_parallel_calls=AUTOTUNE)
filter(p)Keep only elements where p is Truefilter(lambda x, y: y != 3)
shuffle(n)Mix the order using a buffer of n elementsshuffle(10_000)
batch(b)Stack b elements into onebatch(32)
cache()Remember elements after the first pass (in memory or a file)cache() or cache("/tmp/c")
prefetch(n)Prepare the next n elements while the model trainsprefetch(AUTOTUNE)
repeat(k)Go through the data k times (forever if empty)repeat()

Section 02

map, filter and batch

import tensorflow as tf
AUTOTUNE = tf.data.AUTOTUNE

ds = tf.data.Dataset.range(10)
ds = ds.map(lambda x: x * x, num_parallel_calls=AUTOTUNE)   # square each value
ds = ds.filter(lambda x: x % 2 == 0)                        # keep even squares
print("after map+filter:", [int(v) for v in ds])

print("batch(3)        :", [b.numpy().tolist() for b in tf.data.Dataset.range(8).batch(3)])
print("drop_remainder  :", [b.numpy().tolist() for b in tf.data.Dataset.range(8).batch(3, drop_remainder=True)])
OUTPUT
after map+filter: [0, 4, 16, 36, 64] batch(3) : [[0, 1, 2], [3, 4, 5], [6, 7]] drop_remainder : [[0, 1, 2], [3, 4, 5]]
💡
map Runs as a Graph

The function you pass to map is traced into a graph, just like a tf.function. So the rules from the last module apply: use TF ops, not NumPy, and no .numpy() inside. For pure Python code, wrap it in tf.py_function — but it will be slower.


Section 03

shuffle — The Buffer Trick

A dataset may be too big to load all at once, so shuffle(n) keeps a buffer of n elements. Each time you ask for an element, it picks a random one from the buffer and refills the gap with the next element from the input. A small buffer only mixes nearby elements. For a full shuffle, the buffer must be at least as big as the dataset.

base = tf.data.Dataset.range(12)
print("buffer 1  :", [int(v) for v in base.shuffle(1, seed=0)])
print("buffer 3  :", [int(v) for v in base.shuffle(3, seed=0)])
print("buffer 12 :", [int(v) for v in base.shuffle(12, seed=0)])
OUTPUT
buffer 1 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] buffer 3 : [0, 2, 3, 5, 1, 4, 6, 7, 9, 8, 10, 11] buffer 12 : [0, 2, 10, 5, 9, 8, 4, 7, 1, 6, 3, 11]
🎲 Try It — Shuffle Buffer Simulator Interactive

Choose the dataset size and buffer size. Press Play and watch elements enter the buffer, get picked at random, and leave. Green output chips moved far from their starting place; the numbers below measure how well mixed the result is.

input (waiting)
shuffle buffer
output order

Order Matters: shuffle Before batch

print("shuffle then batch:", [b.numpy().tolist() for b in tf.data.Dataset.range(9).shuffle(9, seed=1).batch(3)])
print("batch then shuffle:", [b.numpy().tolist() for b in tf.data.Dataset.range(9).batch(3).shuffle(3, seed=1)])
OUTPUT
shuffle then batch: [[1, 8, 4], [0, 7, 2], [5, 6, 3]] batch then shuffle: [[3, 4, 5], [6, 7, 8], [0, 1, 2]]

Batching first keeps the same neighbours together forever; only the batch order changes. Shuffle before you batch.


Section 04

Speed: parallel map, cache and prefetch

We fake a slow disk read (20 ms per element) and a fast training step (5 ms). The data is the bottleneck. Then we time two epochs with each pipeline. TensorFlow quietly adds some speed tricks by default, so plain() switches them off to give a fair starting point.

import time

def slow_read(i):
    time.sleep(0.02)                    # pretend to read a file
    return i

def load(i):
    return tf.py_function(slow_read, [i], tf.int64)

def plain(ds):                          # turn off automatic tricks for a fair test
    opts = tf.data.Options()
    opts.autotune.enabled = False
    opts.experimental_optimization.inject_prefetch = False
    return ds.with_options(opts)

def time_two_epochs(ds):
    t0 = time.perf_counter()
    for _ in range(2):
        for _ in ds:
            time.sleep(0.005)           # pretend to train on it
    return time.perf_counter() - t0

base = tf.data.Dataset.range(30)
pipelines = {
    "map                     ": base.map(load),
    "map + prefetch          ": base.map(load).prefetch(1),
    "parallel map + prefetch ": base.map(load, num_parallel_calls=4).prefetch(1),
    "parallel + cache + pref.": base.map(load, num_parallel_calls=4).cache().prefetch(1),
}
for name, ds in pipelines.items():
    print(f"{name}: {time_two_epochs(plain(ds)):.2f} s")
OUTPUT — our 2-core CPU test machine
map : 1.57 s map + prefetch : 1.25 s parallel map + prefetch : 0.66 s parallel + cache + pref.: 0.49 s

Each stage helps: prefetch hides the training time behind the reading, parallel map reads 4 files at once, and cache skips the reading completely in epoch 2.

Animated Diagram — Prefetch Overlaps Work
no prefetchprefetch load batch (CPU) train step (GPU)

With prefetch, the CPU prepares batch n+1 while the GPU trains on batch n. The GPU is never idle.


Section 05

Try It — Pipeline Timeline Builder

⏱️ Build a Pipeline, See Where Time Goes Interactive

Set how long it takes to read, preprocess and train on one batch. Turn stages on and off. The timeline shows 2 epochs of 6 batches. Find the settings that keep the GPU busiest.


Section 06

The Recommended Order

ds = (tf.data.Dataset.from_tensor_slices((paths, labels))
        .map(load_and_decode, num_parallel_calls=AUTOTUNE)   # 1. expensive, same every epoch
        .cache()                                             # 2. remember the decoded data
        .shuffle(10_000)                                     # 3. new order every epoch
        .map(random_augment, num_parallel_calls=AUTOTUNE)    # 4. random -> must be AFTER cache
        .batch(32)                                           # 5. group
        .prefetch(AUTOTUNE))                                 # 6. always last
⚠️
Two Classic Ordering Bugs

(1) cache after random augmentation: the first epoch's random crops are frozen and replayed every epoch — no more augmentation. (2) cache after shuffle: the cache stores the first epoch's order and replays it, so every epoch sees the data in the same order.


Section 07

Golden Rules

🍳 Pipeline Methods — Rules to Remember
1
Use num_parallel_calls=tf.data.AUTOTUNE in every expensive map.
2
Shuffle before batch. Use a buffer as large as your memory allows (ideally the dataset size).
3
cache() after deterministic work, before shuffle and random augmentation.
4
End every pipeline with .prefetch(tf.data.AUTOTUNE).
5
Functions inside map are traced into graphs: use TF ops, not Python or NumPy.
You have reached the end of this section. View all sections →