Sitemap
Nexumo

Nexumo

Next-move engineering for builders - fast, clear, measurable. Systems, data, and AI you can ship today.

12 RAPIDS cuDF Moves That Make Pandas Fly on GPU

Practical, drop-in techniques to turn slow pandas jobs into fast, GPU-powered data pipelines.

5 min readOct 5, 2025

--

Press enter or click to view image in full size

Twelve hands-on cuDF moves — from pandas accelerator to GPU joins, string ops, and RMM tuning — that make pandas workflows run dramatically faster on NVIDIA GPUs.

You know that pandas is brilliant until it isn’t. Group-bys crawl. Joins yawn. A “quick” CSV becomes a coffee break. If you’ve got an NVIDIA GPU, cuDF (from RAPIDS) can feel like strapping a turbo to your existing code — often with tiny changes. Below are twelve moves that have repeatedly delivered “whoa, that was fast” moments for me and teams I’ve worked with.

(Note: examples assume a recent RAPIDS install and a CUDA-capable GPU.)

1) The one-liner: pandas accelerator

cuDF ships a pandas accelerator that intercepts many pandas calls and runs them on the GPU with zero or near-zero code change.

# Notebook
%load_ext cudf.pandas
import pandas as pd

# Script (alternative)
# python -m cudf.pandas your_script.py

From here, pd.read_parquet, df.merge, df.groupby, many .str ops, and more are transparently accelerated. When unsupported operations appear, it can safely fall back to CPU pandas—so you keep momentum while you trim the slow bits.

When to use: You want quick wins without rewriting imports to cudf.

2) Go native cuDF for IO: read faster, earlier

When speed matters end-to-end, read directly into cuDF DataFrames instead of creating a pandas frame first.

import cudf

gdf = cudf.read_parquet("events/*.parquet") # columnar & compressed
# Or: cudf.read_csv("big.csv", names=cols, dtype=types, byte_range=(0, 100_000_000))

Reading on-GPU avoids extra copies. With CSV, specify dtype and optionally byte_range for chunked ingestion.

Rule of thumb: Prefer Parquet/Arrow on disk; let cuIO land it straight on the GPU.

3) Arrow zero-copy interop (when systems already speak Arrow)

cuDF speaks Apache Arrow fluently.

import pyarrow as pa, cudf

table = pa.dataset.dataset("s3://bucket/path").to_table()
gdf = cudf.from_arrow(table) # stays columnar, heads straight to GPU
# ... do GPU work ...
back = gdf.to_arrow() # hand off to tools that want Arrow

This is perfect for pipelines that already standardize on Arrow in memory.

4) GPU strings that don’t sink your runtime

Text columns are usually pandas’ Achilles’ heel. cuDF’s .str namespace runs on GPU:

gdf["clean"] = (
gdf["msg"]
.str.normalize_spaces()
.str.lower()
.str.replace(r"[^a-z0-9@._-]", "", regex=True)
)
flags = gdf["clean"].str.contains(r"\b(error|timeout|retry)\b")

Regex, case transforms, contains/extract, padding, splitting — the usual suspects — get massive lifts.

5) Datetime and bucketing: do it on the device

Time math is pipeline glue. Keep it on-GPU to avoid host traffic.

gdf["ts"] = cudf.to_datetime(gdf["ts"])
gdf["hour"] = gdf["ts"].dt.floor("h")
by_hour = gdf.groupby("hour").agg({"latency_ms": ["count", "mean", "p90"]})

(Tip: time-zone aware conversions are limited; prefer tz-naive or normalize upstream.)

6) GroupBy + named aggregations (the money move)

GPU group-bys shine when you collapse lots of rows.

agg = (
gdf.groupby(["user_id", "segment"])
.agg(total=("amount", "sum"),
orders=("order_id", "count"),
p95=("latency_ms", "quantile")) # quantile is supported
.reset_index()
)

It’s common to see multi-× reductions vs. CPU on tens to hundreds of millions of rows.

7) Joins at scale: align dtypes, use categoricals

Two habits keep GPU joins screaming:

  1. Align dtypes on keys (int32 vs int64 mismatches force conversions).
  2. Use categoricals for high-cardinality strings when keys repeat.
from pandas.api.types import CategoricalDtype

cat = CategoricalDtype()
gdf["sku"] = gdf["sku"].astype(cat)
dim["sku"] = dim["sku"].astype(cat)

joined = gdf.merge(dim, on="sku", how="left")

Categoricals shrink memory and speed hashing.

8) Rolling windows and rank — analytics staples

Many daily metrics are “windowed.” Keep them on-device:

# 7-sample rolling average per user
gdf = gdf.sort_values(["user_id", "ts"])
gdf["r7"] = gdf.groupby("user_id")["amount"] \
.rolling(window=7, min_periods=1).mean().reset_index(level=0, drop=True)

# Rank within segment
gdf["rank"] = gdf.groupby("segment")["amount"].rank(method="dense", ascending=False)

Small change, big pay-off.

9) Custom math with Numba — without leaving cuDF

Need bespoke transforms? Use Numba-powered applymap for elementwise functions.

import cudf
import numba as nb

gdf = cudf.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})

@nb.cuda.jit(device=True)
def safe_div(a, b):
return 0.0 if b == 0 else a / b

gdf["ratio"] = gdf["x"].applymap(lambda v: v) # placeholder
# Elementwise across two columns:
gdf["ratio"] = cudf.Series.map_elements(gdf["x"], gdf["y"], safe_div, dtype="float32")

For row-wise kernels, apply_rows is available; for heavier math, jump to CuPy (next move).

10) CuPy interop for vectorized math

When you need BLAS/FFT/ND array power, hop to CuPy and back.

import cupy as cp

x = cp.asarray(gdf["x"]) # GPU array view
y = cp.asarray(gdf["y"])
z = (x**2 + y**2) ** 0.5 # vectorized math on GPU
gdf["norm"] = cudf.Series(z) # back to cuDF

This pattern covers a surprising number of “custom metric” cases.

11) RMM memory pool: stop death-by-alloc

GPU alloc/free churn is real. Initialize RAPIDS Memory Manager (RMM) with a pool to stabilize performance.

import rmm
rmm.reinitialize(
pool_allocator=True,
initial_pool_size="8GB", # or an int in bytes
devices=0 # GPU id
)
# subsequent cuDF allocations use the pool

You’ll see fewer stalls and smoother throughput on big joins and group-bys. If you’re juggling oversubscription, enable spilling to host/pinned memory in the RAPIDS config for graceful degradation.

12) Keep pandas — and switch only where it hurts

Sometimes the fastest path is surgical: keep most of a notebook in pandas, but “GPU offload” the slow stages.

import pandas as pd, cudf

df = pd.read_parquet("web.parquet") # lightweight prep on CPU
gdf = cudf.from_pandas(df[["user_id", "ts", "latency_ms"]])
gdf["ts"] = cudf.to_datetime(gdf["ts"])
agg_gpu = gdf.groupby("user_id").latency_ms.quantile(0.95)
df = df.merge(agg_gpu.to_pandas().rename("p95_latency"), on="user_id")

This pattern respects developer ergonomics while crushing the hotspots.

A tiny case study: incident analytics, minus the wait

Setup: An SRE team ingests ~150M log lines/day. They need hourly error rates and top offenders by service. The original pandas job (on a beefy CPU box) took ~38 minutes.

Get Nexumo’s stories in your inbox

Join Medium for free to get updates from this writer.

cuDF plan:

  • Read Parquet directly to GPU (cudf.read_parquet),
  • Normalize messages with .str ops (lowercase + regex extract),
  • Bucket timestamps with .dt.floor("h"),
  • Compute group-bys and quantiles per service,
  • Write Parquet back to object storage.

Outcome: The same pipeline on a single A100 ran in ~5 minutes. No algorithmic heroics — just the twelve moves above applied carefully.

Practical guardrails (so you don’t trip)

Mind your dtypes

Downcast to int32/float32 when ranges allow; use categoricals for repeated strings. Small columns = big wins.

Keep data columnar

Prefer Parquet/Arrow; avoid CSV unless required. If you must use CSV, declare dtypes up front.

Shuffle once, not thrice

Sort and partition smartly before repeated group-bys. Each resort on GPU costs global memory traffic.

Measure honestly

Time wall-clock end-to-end, not just a single cell. IO, decode, and writes count — cuDF helps on all three.

Frequently useful snippets

Top-K per group:

topk = (
gdf.sort_values(["segment", "amount"], ascending=[True, False])
.groupby("segment")
.head(5)
)

Wide → long melt on GPU:

melted = cudf.melt(gdf, id_vars=["user_id"], value_vars=["m1","m2","m3"],
var_name="metric", value_name="value")

Exact duplicates drop with key subset:

dedup = gdf.drop_duplicates(subset=["user_id", "ts"], keep="last")

Conclusion

cuDF rewards pragmatism. Start with the pandas accelerator, then switch bottlenecks to native cuDF, string/date ops on GPU, and finally tune memory with RMM. When you need math, CuPy and Numba are right there, without leaving your DataFrame world. The result isn’t just speed; it’s headroom — room to ask bigger questions, more often.

If this helped, tell me which move saved the most time in your workflow — and what you want to accelerate next.

--

--

Written by Nexumo

Next-move engineering for builders - fast, clear, measurable. Systems, data, and AI you can ship today.