Performance Engineering · 2026

Optimizing Data Pipelines for GPU Cloud Training

Optimizing Data Pipelines for GPU Cloud Training BHK CLOUD DATA PIPELINE OPTIMIZATION Optimizing Data Pipelines for GPU Cloud Training ENGINEERING NOTES · JULY 2026

Optimizing Data Pipelines for GPU Cloud Training

GPU utilization below 80% during training is a data pipeline problem. The GPU — capable of 35 TFLOPS on an RTX 3090 — waits idle while the CPU loads the next batch from storage, decodes images, applies augmentations, and transfers tensors to VRAM. Every millisecond of idle GPU time is wasted money. This guide covers the architecture and optimization of data pipelines that keep cloud GPUs saturated.

Why Data Pipelines Matter

A training step on a modern GPU takes 100–300 milliseconds for typical models. The CPU must prepare the next batch within that window. If batch preparation takes 350 ms and the GPU step takes 200 ms, the GPU is idle for 150 ms per step — 43% utilization. Over a 10-hour training run, that is 4.3 hours of paid GPU time producing nothing.

The pipeline goal: CPU batch preparation time < GPU step time. When this holds, GPU utilization stays above 95%.

Pipeline Architecture

Single-Process Synchronous (Anti-Pattern)

[Load file] → [Decode] → [Augment] → [Transfer to GPU] → [Train step] → [Repeat]

Each operation blocks the next. GPU waits through the entire CPU pipeline. Utilization: 30–50%.

Multi-Process Prefetching

Process 1: [Load file] → [Decode] → [Augment] → [Queue]
Process 2: [Load file] → [Decode] → [Augment] → [Queue]
Process 3: [Load file] → [Decode] → [Augment] → [Queue]
                                          ↓
                              [Transfer to GPU] → [Train step]

Multiple CPU workers prepare batches in parallel. A prefetch queue decouples CPU work from GPU work. The GPU pulls the next batch from the queue while workers fill future batches. Utilization: 85–95%.

Implementation in PyTorch:

from torch.utils.data import DataLoader

loader = DataLoader(
    dataset,
    batch_size=64,
    num_workers=4,          # CPU processes preparing batches
    pin_memory=True,        # Pin CPU memory for faster GPU transfer
    prefetch_factor=2,      # Prefetch 2 batches per worker
    persistent_workers=True # Keep workers alive between epochs
)

GPU-Direct Storage (Advanced)

On systems with compatible hardware (NVIDIA GPUDirect Storage), data transfers from NVMe directly to GPU memory, bypassing the CPU entirely. Reduces CPU load and latency. Available on A100 and H100 with supported kernels.

Storage Throughput Requirements

The storage system must deliver data at least as fast as the GPU consumes it:

Required throughput = batch_size × sample_size × batches_per_second

Example: training ResNet-50 on ImageNet.

Batch size: 256 images
Sample size: 150 KB (224×224 JPEG)
Batches per second: 5 (200 ms per step)
Required throughput: 256 × 150 KB × 5 = 192 MB/s

Local NVMe at 3 GB/s handles this with room to spare. Object storage at 200 MB/s single-stream would be a bottleneck — but with multi-part parallel downloads, object storage can saturate a 10 Gbps link.

Throughput by Storage Tier

Storage Tier Typical Throughput Max Batches/sec (256×150KB) Verdict
Local NVMe 3,000–7,000 MB/s 78–182 More than sufficient
Network filesystem 500–2,000 MB/s 13–52 Sufficient for most workloads
Object storage (single stream) 100–300 MB/s 2–7 Bottleneck — use staging
Object storage (multi-part, 10 Gbps) ~1,000 MB/s 26 Adequate after staging

Data Format Optimization

Avoid Small Files

A dataset of 1 million 150 KB JPEG images stored as individual files requires the filesystem to handle 1 million inodes. Listing the directory takes seconds. Opening files one at a time generates 1 million system calls.

Solution: archive into sharded tar files (WebDataset) or columnar formats (Lance, Parquet).

WebDataset format:

# Storing: tar archive of images and labels
# dataset.tar:
#   sample_000.jpg, sample_000.cls, sample_001.jpg, sample_001.cls, ...

import webdataset as wds

dataset = wds.WebDataset("s3://bucket/dataset-{000000..000099}.tar")
dataset = dataset.decode("rgb").to_tuple("jpg", "cls")

WebDataset shards of 1 GB each (roughly 7,000 images per shard) reduce filesystem operations by 7,000× compared to individual files.

Compression Trade-offs

Format Size vs Raw Decode Speed GPU Impact
JPEG (quality 90) 5–10× smaller Fast (HW decode) None
WebP 7–15× smaller Fast None
PNG (lossless) 2–3× smaller Slow Bottleneck
TIFF uncompressed 1× (same) Very fast None
NumPy memmap Instant Best for tensors

For image datasets, JPEG at quality 85–90 is the sweet spot — hardware-accelerated decoding on most CPUs, good compression ratio, and imperceptible quality loss for training.

For tensor datasets (embeddings, pre-computed features), NumPy memory-mapped files or Lance format provide zero-copy access.

Augmentation on GPU vs CPU

CPU Augmentation (Standard)

Augmentations (random crop, flip, color jitter, normalization) run on CPU workers before transferring to GPU. Advantages: does not consume GPU compute, simple pipeline. Disadvantages: CPU can become the bottleneck for heavy augmentation pipelines.

GPU Augmentation

Augmentations run on the GPU after transfer. Advantages: GPU is 10–100× faster at image operations, frees CPU for I/O. Disadvantages: consumes GPU memory and compute that could be used for training.

Hybrid approach:

  • Light augmentations on CPU: decoding, resizing, random crop, flip
  • Heavy augmentations on GPU: MixUp, CutMix, RandAugment

Tools: NVIDIA DALI (CPU+GPU augmentation pipeline), Kornia (GPU augmentations in PyTorch).

Multi-Node Data Loading

When training across 4+ GPUs on multiple nodes, data loading must scale:

  • Replicated dataset. Each node has a full copy of the dataset on local NVMe. Simplest approach. Storage cost: dataset_size × num_nodes. Only feasible for datasets under 500 GB.
  • Shared network filesystem. All nodes read from the same NFS/Lustre mount. Lower storage cost but network filesystem throughput becomes the bottleneck at 4–8 GPUs.
  • Streaming from object storage. Each node streams its shard of the dataset from object storage. MosaicML StreamingDataset and WebDataset support this natively. Scales to arbitrary node counts because object storage throughput scales horizontally.

Profiling the Pipeline

Identify bottlenecks before optimizing:

import time

for batch_idx, (data, target) in enumerate(loader):
    t0 = time.time()
    data, target = data.cuda(), target.cuda()
    t1 = time.time()
    output = model(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()
    t2 = time.time()

    if batch_idx % 100 == 0:
        transfer_time = t1 - t0
        compute_time = t2 - t1
        print(f"Transfer: {transfer_time*1000:.1f}ms, Compute: {compute_time*1000:.1f}ms")

If transfer_time > compute_time × 0.2, the data pipeline is the bottleneck. Increase num_workers, switch to a faster storage tier, or reduce sample size.

Frequently Asked Questions

How many DataLoader workers should I use?

Start with num_workers = CPU cores / 2. For a typical cloud GPU instance with 8 vCPUs, start with 4 workers. Monitor CPU utilization. If CPU is below 70% and GPU utilization is below 90%, add more workers. If CPU is at 100%, reduce workers or optimize augmentation code.

Should I store datasets compressed or uncompressed?

Compressed for storage and transfer (JPEG, WebP for images; gzip/zstd for text). Decompress at load time in the DataLoader worker. The I/O savings from compression (5–10× smaller files) almost always outweigh the CPU cost of decompression — especially for image datasets where JPEG decoding is hardware-accelerated.

Does object storage latency matter for training?

Not if you stage data to local NVMe before training. Reading from object storage during training introduces 10–50 ms latency per object — unacceptable for per-batch I/O. Stage-to-NVMe is the only viable pattern for object-storage-backed training.

What is the fastest data format for PyTorch training?

For images: WebDataset (tar archives) served from local NVMe. For tensors: memory-mapped NumPy arrays served from local NVMe. For text: pre-tokenized Arrow/Parquet files streamed with MosaicML StreamingDataset. In all cases, local NVMe with sequential read patterns achieves 3–7 GB/s — sufficient for any batch size.

BHK Cloud offers NVMe SSD-equipped RTX 3090 instances at $0.15/hr with per-minute billing. Stage datasets from S3-compatible storage at $2.49/TB/month to local NVMe and keep your GPU saturated. Rent a GPU or set up storage.

GPU StorageData PipelinesPyTorchDataLoaderThroughput