Step 1: cuDF vs pandas on a realistic groupby
Every data scientist spends ~80% of their time on data munging and model iteration. RAPIDS, announced by NVIDIA in October 2018 at GTC, took the two most-used libraries in that workflow — pandas and scikit-learn — and reimplemented their APIs to run on GPU:
- cuDF — pandas-compatible DataFrame, running on GPU via CUDA.
- cuML — scikit-learn-compatible ML algorithms (KMeans, RandomForest, UMAP, t-SNE, ...) running on GPU.
The pitch: your existing pandas/sklearn code, with minimal changes, suddenly 10–100× faster.
What the real speedups look like
- cuDF vs pandas: official benchmarks show up to 30× speedup on joins for a 10 GB dataset (NVIDIA T4).
- cuML vs sklearn: 2–80× on typical tasks, up to 300× for UMAP on large datasets.
- End-to-end workflows (load → preprocess → train): ~44× on an RTX 8000.
Important caveat: for small data, GPUs lose. The PCIe transfer overhead plus kernel launch latency dominates. The break-even point is typically around 100K rows for simple ops, 1M+ for complex joins. You'll see this yourself in this lab.
When RAPIDS is worth it
- Large tabular datasets (millions of rows, wide features)
- Dimensionality reduction (UMAP, t-SNE, PCA)
- Classical ML where you currently wait 5+ min for sklearn to fit
- Feature engineering at data-warehouse scale
When it's not
- Tiny datasets (< 100K rows)
- Deep learning (just use PyTorch directly — tensors already on GPU)
- Graph analytics (use cuGraph instead of cuML for that)
Read while this runs
- NVIDIA blog — Accelerated Data Analytics — quick-start + production use cases.
- RAPIDS cuDF performance comparisons — official benchmarks.
Groupby aggregation is the workhorse of data prep. We'll generate a synthetic e-commerce-ish dataset (1 million rows) and time the same groupby-aggregate on both pandas and cuDF.
Two things to notice:
- The cuDF API is nearly identical to pandas. You import
cudf as pdand most code ports without changes. - For a million rows on a 3060 Ti, cuDF should be roughly 5–15× faster than pandas on this op. The exact speedup varies by op; joins see larger gains than element-wise ops.
import numpy as np
import pandas as pd
import cudf
import time
# Build a realistic-ish 1M-row dataset (e-commerce events)
N = 1_000_000
rng = np.random.default_rng(42)
data = {
'user_id': rng.integers(0, 50_000, N),
'product_id': rng.integers(0, 2_000, N),
'country': rng.choice(['US', 'UK', 'DE', 'FR', 'JP', 'IN', 'BR'], N),
'amount': rng.exponential(scale=40, size=N).astype('float32'),
'is_refund': (rng.random(N) < 0.03).astype('int8'),
}
pdf = pd.DataFrame(data) # host/CPU dataframe
gdf = cudf.from_pandas(pdf) # device/GPU dataframe (modern cuDF API)
print(f'Rows: {len(gdf):,} | Columns: {list(gdf.columns)}')
print(f'GPU memory used by dataframe: ~{gdf.memory_usage().sum() / 1e6:.1f} MB')
print()
print(gdf.head())
# Same query on pandas and cuDF — identical syntax
def run_groupby(df):
return (
df.groupby(['country', 'is_refund'])
.agg({'amount': ['sum', 'mean', 'count']})
.sort_values(('amount', 'sum'), ascending=False)
)
# pandas timing
_ = run_groupby(pdf) # warmup caches
t0 = time.perf_counter(); _ = run_groupby(pdf); pandas_ms = (time.perf_counter() - t0) * 1000
# cuDF timing
_ = run_groupby(gdf) # warmup JIT / kernel compile
import cupy
cupy.cuda.Stream.null.synchronize()
t0 = time.perf_counter()
groupby_result = run_groupby(gdf)
cupy.cuda.Stream.null.synchronize()
cudf_ms = (time.perf_counter() - t0) * 1000
print(f'pandas: {pandas_ms:6.1f} ms')
print(f'cuDF: {cudf_ms:6.1f} ms')
print(f'Speedup: {pandas_ms / max(cudf_ms, 0.01):.1f}x')
print()
print('cuDF result:')
print(groupby_result.head())