hands-on labon a real NVIDIA GPU

NVIDIA DALI: GPU-Accelerated Data Pipelines

Move image decoding, resizing, and augmentation from CPU to GPU with NVIDIA DALI, and benchmark it against a standard PyTorch DataLoader.

time
30 min
checked steps
4
level
Intermediate
setup
None
NCP-GENLNCP-ADSNCA-GENL

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

Lab cockpit30 min · 4 stepsSession running
3 / 4 steps passingWhen is DALI worth the engineering cost? · step 4 of 4
lab.ipynb

Step 4 — Think clearly: when is DALI worth the engineering cost?

DALI has real costs:

[ ]:
# Fill these in with YOUR reasoning. Examples provided; add at least 3 wins and 2 losses total.where_dali_wins = [    'ImageNet-scale training on 4+ GPUs (data pipeline becomes the bottleneck without GPU offload)',    'Video training: decoding MP4/frames is CPU-expensive; DALI has hardware-accelerated video decode',    'Production inference server doing image preprocessing at high QPS (nvJPEG scales with request rate)',    'Any workload where nvidia-smi consistently shows GPU <70% utilization during training',] where_dali_loses = [    'Small datasets (CIFAR, MNIST) — CPU pipeline is already fast enough',    'NLP/text workloads — tokenization is cheap; CPU is plenty',    'Development / experimentation where pipeline flexibility beats raw throughput',] print('DALI is worth it when:')for w in where_dali_wins: print(f'  + {w}')print('\nDALI is overkill when:')for l in where_dali_loses: print(f'  - {l}')
[ ]:
lab.check(4)
step 4 of 4, replayed: When is DALI worth the engineering cost?. The code, the output and the check are the lab's own.

4 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Baseline: PyTorch DataLoader + torchvision transforms

    read this step below ↓

  2. 2

    Build a DALI pipeline (GPU decoding + augmentation)

  3. 3

    Benchmark DALI against the PyTorch baseline

  4. 4

    When is DALI worth the engineering cost?

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Baseline: PyTorch DataLoader + torchvision transforms

In Lab I-1 you saw the data-loader bottleneck: GPU at 30% utilization while the CPU struggles to decode JPEGs and apply augmentations. The classic fix is num_workers=8 on PyTorch's DataLoader, which helps — but you're still pinning 8 CPU cores to a job the GPU could do faster.

NVIDIA DALI (Data Loading Library) moves decoding, resizing, color-space conversion, cropping, and normalization onto the GPU itself via nvJPEG and custom CUDA kernels. Once DALI is in the pipeline, the CPU just reads bytes off disk; everything else happens on-device. The practical effect on real ImageNet-scale training:

PipelineImages/sec (8×A100)
Native PyTorch DataLoader, 16 workers~8,000
DALI GPU pipeline~30,000–50,000

That's a 4–6× throughput multiplier on data-pipeline-bound workloads. For any serious multi-GPU training setup, DALI is not optional.

When DALI is worth it (and when it isn't)

  • Wins big: Vision workloads on multi-GPU, large images (ImageNet/COCO/video), aggressive augmentation
  • Marginal gain: Small images (CIFAR-sized), tiny models, dev-machine single-GPU experiments
  • Overkill: NLP workloads (tokenization is fast; CPU is plenty). Reinforcement learning (env step, not data pipe, is the bottleneck)

This lab runs on a single 8 GB 3060 Ti, so we'll see a modest speedup — enough to demonstrate the pattern. On an A100 with 8× the compute, the relative gap grows dramatically.

References

What you'll build

  1. Synthesize a ~1,000-JPEG dataset; measure PyTorch DataLoader baseline throughput
  2. Build a DALI pipeline that reads from disk and processes on GPU
  3. Benchmark DALI; compute the speedup
  4. Reason about when DALI is worth the engineering investment

We'll generate a tiny synthetic JPEG dataset (512 images, 224×224) and use a standard PyTorch DataLoader with torchvision transforms. This is the canonical 'ship it' pipeline in ~90% of PyTorch tutorials.

import os, shutil, time
import numpy as np
from PIL import Image

# Synth dataset: random RGB noise saved as JPEGs. Mimics an image-classification input.
dataset_dir = '/tmp/dali_dataset'
if os.path.exists(dataset_dir):
    shutil.rmtree(dataset_dir)
os.makedirs(dataset_dir + '/class0', exist_ok=True)

N_IMAGES = 512
IMG_SIZE = 224
for i in range(N_IMAGES):
    # Random noise image
    arr = np.random.randint(0, 255, (IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8)
    Image.fromarray(arr).save(f'{dataset_dir}/class0/{i:04d}.jpg', quality=85)

total_bytes = sum(os.path.getsize(f'{dataset_dir}/class0/{f}') for f in os.listdir(dataset_dir + '/class0'))
print(f'Created {N_IMAGES} JPEGs in {dataset_dir} ({total_bytes/1024/1024:.1f} MB total)')

The CPU-bound baseline — torchvision + DataLoader

The standard PyTorch data pipeline: a Dataset that reads files one at a time and applies transforms.Compose(...) entirely on the CPU. Works everywhere, but every op is CPU: JPEG decode, resize, random crop, flip, normalize. On a fast GPU the CPU becomes the bottleneck — you'll see GPU util dip to 20-40% while the dataloader catches up.

# Baseline: PyTorch Dataset/DataLoader with torchvision transforms (all CPU)
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms

class JPEGDataset(Dataset):
    def __init__(self, root, transform):
        self.files = sorted(os.path.join(root, 'class0', f) for f in os.listdir(os.path.join(root, 'class0')) if f.endswith('.jpg'))
        self.transform = transform
    def __len__(self): return len(self.files)
    def __getitem__(self, i):
        img = Image.open(self.files[i]).convert('RGB')
        return self.transform(img)

# Realistic production-style CPU transforms
tx = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

ds = JPEGDataset(dataset_dir, tx)
loader = DataLoader(ds, batch_size=32, num_workers=0, pin_memory=True)

Measure — 2 full epochs

Warm the pipeline once (first iter pays worker spawn + pipeline fill overhead), then time 2 full epochs. torch.cuda.synchronize() after each batch makes the wall-clock measurement reflect real end-to-end throughput.

# Warm up (let workers spawn + pipeline fill)
for b in loader:
    b.cuda(non_blocking=True)
    break

# Benchmark: time 2 full epochs
n_images = 0
t0 = time.perf_counter()
for _ in range(2):
    for b in loader:
        b = b.cuda(non_blocking=True)
        torch.cuda.synchronize()
        n_images += b.shape[0]
pytorch_throughput = n_images / (time.perf_counter() - t0)
print(f'PyTorch DataLoader + torchvision (CPU): {pytorch_throughput:.1f} images/sec')

what you get

  • A JupyterLab notebook on a real NVIDIA GPU
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Comfortable with PyTorch DataLoader and torchvision transforms
  • Basic understanding of JPEG decoding and image augmentation
  • CUDA-capable GPU with a working PyTorch installation

Exam domains covered

GPU Acceleration & Distributed TrainingGPU Infrastructure & Operations

Part of the GPU and Kubernetes track

Lab 18 of 20. GPU scheduling on your own Kubernetes cluster, the GPU Operator, CUDA kernels and profiling.

Questions about this lab

Is DALI worth it if I'm not training on ImageNet?

Depends on whether your pipeline is CPU-bound. If nvidia-smi shows sustained sub-70% GPU utilization while your DataLoader workers are pegged, you're starved and DALI helps. If you're fine-tuning BERT where the 'preprocessing' is tokenization, almost no CPU work, DALI gives you nothing. The Step 4 reflection asks for this analysis explicitly: define the acceptance metric (images/sec at fixed batch size, or wall-clock epoch time) before you integrate, so you can tell afterward whether it paid off.

What's nvJPEG and why does it matter?

nvJPEG is NVIDIA's GPU-accelerated JPEG decoder, it reads compressed bytes and produces decoded RGB tensors directly in GPU memory, bypassing libjpeg-turbo on the CPU. Dedicated hardware decoders on newer GPUs make this dramatically faster than a CPU decode + H2D copy, and it's what fn.decoders.image with device='mixed' triggers in a DALI pipeline. The Step 2 pipeline uses it implicitly; you're not wiring it by hand, but it's the piece doing the heavy lifting.

Why does the PyTorch DataLoader look slow in this lab?

Because the Step 1 baseline intentionally uses the simplest possible configuration, one worker, synchronous CPU transforms, so the comparison is clean. A fully-tuned PyTorch DataLoader with num_workers=8, pin_memory=True, non_blocking=True, prefetch_factor=2 closes most of the gap for many workloads. The point of the lab isn't 'DALI > DataLoader' unconditionally, it's measuring the delta on a controlled workload and then thinking carefully about where the advantage transfers.

What's the warmup step about in Step 3?

DALI pays pipeline-prefill latency on the very first batch, building the execution graph, warming the CUDA kernels for each op, kicking off the async prefetcher. If you time starting from iteration zero, that prefill amortizes badly over a tiny test set and DALI looks slower than it is. One full warmup iteration, then time the real epochs, exactly the same discipline you'd apply to any async GPU benchmark, including the PyTorch baseline.

Can I mix DALI with my existing PyTorch training loop?

Yes, and that's the whole point of DALIGenericIterator (or DALIClassificationIterator for classification, or the PyTorch Lightning adapters). It yields dicts/lists of CUDA tensors that drop directly into your existing forward pass, you replace the DataLoader, nothing else. For multi-GPU you'd use DALIGenericIterator with num_shards/shard_id matching your DDP rank so each process reads its own shard of the dataset.

What does the grader actually check?

Step 1 walks dataset_dir recursively for ≥100 JPEG/JPEG files and verifies pytorch_throughput > 0. Step 2 confirms the DALI iterator was built and first_batch is a 4D CUDA tensor with batch size ≥16. Step 3 enforces dali_throughput > pytorch_throughput * 1.1, a modest floor that makes sure both pipelines ran on the same data. Step 4 requires where_dali_wins with ≥3 scenarios and where_dali_loses with ≥2 anti-cases as strings.

What you'll build in this NVIDIA DALI lab

Input-pipeline starvation is the silent killer of GPU utilization, the moment you scale to 4+ GPUs, the same CPU-side JPEG decode that looked fine on one card collapses into 40-70% idle SMs while your DataLoader workers max out CPU cores. DALI is the fix engineers reach for when nvidia-smi says the GPU is bored and the CPU is on fire. You'll walk away with a working GPU-native preprocessing pipeline built on nvJPEG + CUDA augmentations, an honest before/after throughput delta (typically 1.3-3× on a single card, 3-10× once multiple GPUs share one host), a DALIGenericIterator you can drop into an existing PyTorch training loop, and, critically, the judgment to know when DALI is overkill versus a 10× win. About 30 minutes on a live NVIDIA GPU pod we hand you; PyTorch, DALI, and nvJPEG are already installed.

The substance is the pipeline wiring: @pipeline_def(batch_size=32, num_threads=4, device_id=0) with fn.readers.file → fn.decoders.image(device='mixed') → fn.resize → fn.random_resized_crop → fn.crop_mirror_normalize(output_layout='CHW'). device='mixed' is the magic word, it routes JPEG decode to nvJPEG's dedicated hardware decoders and keeps every downstream op on the GPU, so there's no H2D copy of raw pixels and no CPU-side PIL bottleneck. The trap most teams hit: DALI pays pipeline-prefill cost on the first batch (CUDA kernel warmup, async prefetcher kickoff), so if you skip warmup your benchmark makes DALI look slower than torchvision. The other trap: DALI only helps data-pipeline-bound workloads, vision training on A100s wins big, but BERT fine-tuning (tokenization is cheap, there's no preprocessing) sees zero benefit. You'll measure both and build a mental model for where the advantage actually transfers.

Prereqs: PyTorch DataLoader and torchvision transforms, a rough sense of JPEG decoding and augmentation pipelines, a CUDA-capable GPU. Preinstalled: PyTorch, NVIDIA DALI, nvJPEG bindings, JupyterLab. The grader enforces real behavior rather than code-shape matching, the dataset must have ≥100 JPEGs under dataset_dir, the first DALI batch must arrive on CUDA as a 4D tensor with batch size ≥16, dali_throughput must exceed pytorch_throughput by at least 1.1×, and you have to produce substantive where_dali_wins / where_dali_loses lists that show you understand the modality dependency.