Skill Tracer
Skills › Scientific Agent Skills › optimize-for-gpu › code_transformation_patterns.md

code_transformation_patterns.md

308 lines · skills/optimize-for-gpu/references/code_transformation_patterns.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Code Transformation Patterns
2
3Before/after conversions: NumPy to CuPy, pandas to cuDF, a custom loop to a Numba CUDA
4kernel, NetworkX to cuGraph, scikit-learn to cuML, a simulation loop to a Warp kernel,
5file IO to KvikIO, maintained GPU-backed dashboards, scikit-image to cuCIM, legacy
6GeoPandas-to-cuSpatial point-in-polygon, exact Faiss to exact cuVS search, and
7`scipy.sparse.linalg` to RAFT.
8
9These are illustrative patterns, not executed GPU benchmarks. Reuse the same inputs and
10validate outputs before timing; independently generated CPU/GPU random arrays are not
11a correctness comparison. Installation targets and hardware limits are in `installation.md`.
13When converting existing CPU code, apply these patterns:

Code Transformation Patterns

Before/after conversions: NumPy to CuPy, pandas to cuDF, a custom loop to a Numba CUDA kernel, NetworkX to cuGraph, scikit-learn to cuML, a simulation loop to a Warp kernel, file IO to KvikIO, maintained GPU-backed dashboards, scikit-image to cuCIM, legacy GeoPandas-to-cuSpatial point-in-polygon, exact Faiss to exact cuVS search, and scipy.sparse.linalg to RAFT.

These are illustrative patterns, not executed GPU benchmarks. Reuse the same inputs and validate outputs before timing; independently generated CPU/GPU random arrays are not a correctness comparison. Installation targets and hardware limits are in installation.md.

When converting existing CPU code, apply these patterns:

Raw lines 1 to 14
15### NumPy to CuPy
16```python
17# Before (CPU)
18import numpy as np
19a = np.random.rand(10_000_000)
20b = np.fft.fft(a)
21c = np.sort(b.real)
23# After (GPU) — often just change the import
24import cupy as cp
25a = cp.random.rand(10_000_000)
26b = cp.fft.fft(a)
27c = cp.sort(b.real)
28```

NumPy to CuPy

# Before (CPU)
import numpy as np
a = np.random.rand(10_000_000)
b = np.fft.fft(a)
c = np.sort(b.real)

# After (GPU) — often just change the import
import cupy as cp
a = cp.random.rand(10_000_000)
b = cp.fft.fft(a)
c = cp.sort(b.real)
Raw lines 15 to 29
30### pandas to cuDF
31```python
32# Before (CPU)
33import pandas as pd
34df = pd.read_parquet("large_data.parquet")
35result = df.groupby("category")["value"].mean()
37# After (GPU) — change the import
38import cudf
39df = cudf.read_parquet("large_data.parquet")
40result = df.groupby("category")["value"].mean()
42# Or zero-code-change: python -m cudf.pandas your_script.py
43```

pandas to cuDF

# Before (CPU)
import pandas as pd
df = pd.read_parquet("large_data.parquet")
result = df.groupby("category")["value"].mean()

# After (GPU) — change the import
import cudf
df = cudf.read_parquet("large_data.parquet")
result = df.groupby("category")["value"].mean()

# Or zero-code-change: python -m cudf.pandas your_script.py
Raw lines 30 to 44
45### Custom loop to Numba CUDA kernel
46```python
47# Before (CPU) — slow Python loop
48def process(data, out):
49 for i in range(len(data)):
50 out[i] = math.sin(data[i]) * math.exp(-data[i])
52# After (GPU) — Numba kernel
53from numba import cuda
54import math
56@cuda.jit
57def process(data, out):
58 i = cuda.grid(1)
59 if i < data.size:
60 out[i] = math.sin(data[i]) * math.exp(-data[i])
62d_data = cuda.to_device(data)
63d_out = cuda.device_array(d_data.shape, dtype=d_data.dtype)
64threads = 256
65blocks = (len(data) + threads - 1) // threads
66process[blocks, threads](d_data, d_out)
67out = d_out.copy_to_host()
68```

Custom loop to Numba CUDA kernel

# Before (CPU) — slow Python loop
def process(data, out):
    for i in range(len(data)):
        out[i] = math.sin(data[i]) * math.exp(-data[i])

# After (GPU) — Numba kernel
from numba import cuda
import math

@cuda.jit
def process(data, out):
    i = cuda.grid(1)
    if i < data.size:
        out[i] = math.sin(data[i]) * math.exp(-data[i])

d_data = cuda.to_device(data)
d_out = cuda.device_array(d_data.shape, dtype=d_data.dtype)
threads = 256
blocks = (len(data) + threads - 1) // threads
process[blocks, threads](d_data, d_out)
out = d_out.copy_to_host()
Raw lines 45 to 69
70### NetworkX to cuGraph
71```python
72# Before (CPU)
73import networkx as nx
74G = nx.read_edgelist("edges.csv", delimiter=",", nodetype=int)
75pr = nx.pagerank(G)
76bc = nx.betweenness_centrality(G)
78# After (GPU) — direct cuGraph API
79import cugraph
80import cudf
81edges = cudf.read_csv("edges.csv", names=["src", "dst"], dtype=["int32", "int32"])
82G = cugraph.Graph()
83G.from_cudf_edgelist(edges, source="src", destination="dst")
84pr = cugraph.pagerank(G)
85bc = cugraph.betweenness_centrality(G)
87# Or zero-code-change: NX_CUGRAPH_AUTOCONFIG=True python your_script.py
88```

NetworkX to cuGraph

# Before (CPU)
import networkx as nx
G = nx.read_edgelist("edges.csv", delimiter=",", nodetype=int)
pr = nx.pagerank(G)
bc = nx.betweenness_centrality(G)

# After (GPU) — direct cuGraph API
import cugraph
import cudf
edges = cudf.read_csv("edges.csv", names=["src", "dst"], dtype=["int32", "int32"])
G = cugraph.Graph()
G.from_cudf_edgelist(edges, source="src", destination="dst")
pr = cugraph.pagerank(G)
bc = cugraph.betweenness_centrality(G)

# Or zero-code-change: NX_CUGRAPH_AUTOCONFIG=True python your_script.py
Raw lines 70 to 89
Runs code python your_script.py :87
90### scikit-learn to cuML
91```python
92# Before (CPU)
93from sklearn.ensemble import RandomForestClassifier
94from sklearn.preprocessing import StandardScaler
95from sklearn.model_selection import train_test_split
97X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
98scaler = StandardScaler()
99X_train = scaler.fit_transform(X_train)
100model = RandomForestClassifier(n_estimators=100)
101model.fit(X_train, y_train)
103# After (GPU) — match parameters and held-out quality; algorithms need not be identical
104from cuml.ensemble import RandomForestClassifier
105from cuml.preprocessing import StandardScaler
106from cuml.model_selection import train_test_split
108X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
109scaler = StandardScaler()
110X_train = scaler.fit_transform(X_train)
111model = RandomForestClassifier(n_estimators=100)
112model.fit(X_train, y_train)
114# Or zero-code-change: python -m cuml.accel your_script.py
115```

scikit-learn to cuML

# Before (CPU)
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)

# After (GPU) — match parameters and held-out quality; algorithms need not be identical
from cuml.ensemble import RandomForestClassifier
from cuml.preprocessing import StandardScaler
from cuml.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)

# Or zero-code-change: python -m cuml.accel your_script.py
Raw lines 90 to 116
117### Simulation loop to Warp kernel
118```python
119# Before (CPU) — slow Python loop over particles
120import numpy as np
122def integrate(positions, velocities, forces, dt):
123 for i in range(len(positions)):
124 velocities[i] += forces[i] * dt # Unit mass, or forces stores accelerations
125 positions[i] += velocities[i] * dt
127# After (GPU) — Warp kernel, JIT-compiled to CUDA
128import warp as wp
130@wp.kernel
131def integrate(positions: wp.array(dtype=wp.vec3),
132 velocities: wp.array(dtype=wp.vec3),
133 forces: wp.array(dtype=wp.vec3),
134 dt: float):
135 tid = wp.tid()
136 velocities[tid] = velocities[tid] + forces[tid] * dt
137 positions[tid] = positions[tid] + velocities[tid] * dt
139wp.launch(integrate, dim=num_particles,
140 inputs=[positions, velocities, forces, 0.01], device="cuda")
141```

Simulation loop to Warp kernel

# Before (CPU) — slow Python loop over particles
import numpy as np

def integrate(positions, velocities, forces, dt):
    for i in range(len(positions)):
        velocities[i] += forces[i] * dt  # Unit mass, or forces stores accelerations
        positions[i] += velocities[i] * dt

# After (GPU) — Warp kernel, JIT-compiled to CUDA
import warp as wp

@wp.kernel
def integrate(positions: wp.array(dtype=wp.vec3),
              velocities: wp.array(dtype=wp.vec3),
              forces: wp.array(dtype=wp.vec3),
              dt: float):
    tid = wp.tid()
    velocities[tid] = velocities[tid] + forces[tid] * dt
    positions[tid] = positions[tid] + velocities[tid] * dt

wp.launch(integrate, dim=num_particles,
          inputs=[positions, velocities, forces, 0.01], device="cuda")
Raw lines 117 to 142
143### File IO to GPU with KvikIO
144```python
145# Before — CPU staging (disk → CPU → GPU)
146import numpy as np
147import cupy as cp
149data = np.fromfile("data.bin", dtype=np.float32)
150gpu_data = cp.asarray(data) # Extra copy through CPU memory
152# After — device destination (local GDS only when configured and eligible)
153import cupy as cp
154import kvikio
156gpu_data = cp.empty(1_000_000, dtype=cp.float32)
157with kvikio.CuFile("data.bin", "r") as f:
158 f.read(gpu_data) # May use host staging in compatibility mode
160# Reading from S3 directly to GPU
161with kvikio.RemoteFile.open_s3_url("s3://bucket/data.bin") as f:
162 buf = cp.empty(f.nbytes() // 4, dtype=cp.float32)
163 f.read(buf)
164```

File IO to GPU with KvikIO

# Before — CPU staging (disk → CPU → GPU)
import numpy as np
import cupy as cp

data = np.fromfile("data.bin", dtype=np.float32)
gpu_data = cp.asarray(data)  # Extra copy through CPU memory

# After — device destination (local GDS only when configured and eligible)
import cupy as cp
import kvikio

gpu_data = cp.empty(1_000_000, dtype=cp.float32)
with kvikio.CuFile("data.bin", "r") as f:
    f.read(gpu_data)  # May use host staging in compatibility mode

# Reading from S3 directly to GPU
with kvikio.RemoteFile.open_s3_url("s3://bucket/data.bin") as f:
    buf = cp.empty(f.nbytes() // 4, dtype=cp.float32)
    f.read(buf)
Raw lines 143 to 165
166### GPU-backed dashboard with maintained libraries
168cuxfilter ended with RAPIDS 26.06. Do not start a new application with it. Keep large
169transformations and aggregations in cuDF, then transfer only the compact display data at an
170explicit visualization boundary:
172```python
173# Before — static matplotlib/seaborn plots, no interactivity
174import pandas as pd
175import matplotlib.pyplot as plt
177df = pd.read_parquet("large_dataset.parquet")
178fig, axes = plt.subplots(1, 2)
179df.plot.scatter(x="feature1", y="feature2", ax=axes[0])
180df["category"].value_counts().plot.bar(ax=axes[1])
181plt.show()
183# After — GPU data preparation plus a maintained dashboard stack
184import cudf
185import hvplot.pandas # Registers .hvplot on pandas objects
186import panel as pn
188gpu_df = cudf.read_parquet("large_dataset.parquet")
189gpu_summary = (
190 gpu_df.groupby("category", as_index=False)
191 .agg({"value_col": "mean"})
193display_summary = gpu_summary.to_pandas() # Transfer only the reduced result
194dashboard = pn.Column(
195 "# Interactive Explorer",
196 display_summary.hvplot.bar(x="category", y="value_col"),
198dashboard.servable()
199```
201For linked selections over detailed points, use HoloViews/hvPlot with Datashader and Panel.
202Keep filter/aggregation callbacks on the GPU where practical, and document every conversion to
203pandas. Read the cuxfilter reference only when maintaining an existing 26.06 application.

GPU-backed dashboard with maintained libraries

cuxfilter ended with RAPIDS 26.06. Do not start a new application with it. Keep large transformations and aggregations in cuDF, then transfer only the compact display data at an explicit visualization boundary:

# Before — static matplotlib/seaborn plots, no interactivity
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_parquet("large_dataset.parquet")
fig, axes = plt.subplots(1, 2)
df.plot.scatter(x="feature1", y="feature2", ax=axes[0])
df["category"].value_counts().plot.bar(ax=axes[1])
plt.show()

# After — GPU data preparation plus a maintained dashboard stack
import cudf
import hvplot.pandas  # Registers .hvplot on pandas objects
import panel as pn

gpu_df = cudf.read_parquet("large_dataset.parquet")
gpu_summary = (
    gpu_df.groupby("category", as_index=False)
    .agg({"value_col": "mean"})
)
display_summary = gpu_summary.to_pandas()  # Transfer only the reduced result
dashboard = pn.Column(
    "# Interactive Explorer",
    display_summary.hvplot.bar(x="category", y="value_col"),
)
dashboard.servable()

For linked selections over detailed points, use HoloViews/hvPlot with Datashader and Panel. Keep filter/aggregation callbacks on the GPU where practical, and document every conversion to pandas. Read the cuxfilter reference only when maintaining an existing 26.06 application.

Raw lines 166 to 204
205### scikit-image to cuCIM
206```python
207# Before (CPU)
208from skimage.filters import gaussian, sobel, threshold_otsu
209from skimage.morphology import binary_opening, disk
210from skimage.measure import label, regionprops_table
211import numpy as np
213blurred = gaussian(image, sigma=3)
214binary = blurred > threshold_otsu(blurred)
215cleaned = binary_opening(binary, footprint=disk(3))
216labels = label(cleaned)
217props = regionprops_table(labels, image, properties=['area', 'centroid'])
219# After (GPU) — change imports, wrap input with cp.asarray
220from cucim.skimage.filters import gaussian, sobel, threshold_otsu
221from cucim.skimage.morphology import binary_opening, disk
222from cucim.skimage.measure import label, regionprops_table
223import cupy as cp
225image_gpu = cp.asarray(image) # Transfer once
226blurred = gaussian(image_gpu, sigma=3)
227binary = blurred > threshold_otsu(blurred)
228cleaned = binary_opening(binary, footprint=disk(3))
229labels = label(cleaned)
230props = regionprops_table(labels, image_gpu, properties=['area', 'centroid'])
231```

scikit-image to cuCIM

# Before (CPU)
from skimage.filters import gaussian, sobel, threshold_otsu
from skimage.morphology import binary_opening, disk
from skimage.measure import label, regionprops_table
import numpy as np

blurred = gaussian(image, sigma=3)
binary = blurred > threshold_otsu(blurred)
cleaned = binary_opening(binary, footprint=disk(3))
labels = label(cleaned)
props = regionprops_table(labels, image, properties=['area', 'centroid'])

# After (GPU) — change imports, wrap input with cp.asarray
from cucim.skimage.filters import gaussian, sobel, threshold_otsu
from cucim.skimage.morphology import binary_opening, disk
from cucim.skimage.measure import label, regionprops_table
import cupy as cp

image_gpu = cp.asarray(image)  # Transfer once
blurred = gaussian(image_gpu, sigma=3)
binary = blurred > threshold_otsu(blurred)
cleaned = binary_opening(binary, footprint=disk(3))
labels = label(cleaned)
props = regionprops_table(labels, image_gpu, properties=['area', 'centroid'])
Raw lines 205 to 232
233### GeoPandas point-in-polygon to cuSpatial (legacy 25.04 only)
235cuSpatial is archived and incompatible with current RAPIDS packages. Use this only in an isolated
236environment pinned to 25.04. `point_in_polygon` returns a boolean membership matrix; it is not a
237drop-in replacement for `geopandas.sjoin`.
239```python
240# Before (CPU)
241import geopandas as gpd
242import numpy as np
243from shapely.geometry import Point
245points = gpd.GeoSeries([Point(x, y) for x, y in coords], crs="EPSG:4326")
246polygons = gpd.read_file("regions.geojson").geometry.iloc[:31]
247polygons = polygons.to_crs(points.crs)
248if any(points.intersects(polygon.boundary).any() for polygon in polygons):
249 raise ValueError("Validate a boundary predicate explicitly before this port")
250membership_cpu = np.column_stack(
251 [points.within(polygon).to_numpy() for polygon in polygons]
254# After (GPU, legacy) — point-by-polygon membership on these non-boundary inputs
255import cuspatial
257points_gpu = cuspatial.from_geopandas(points)
258polygons_gpu = cuspatial.from_geopandas(polygons)
259membership_gpu = cuspatial.point_in_polygon(points_gpu, polygons_gpu)
260```

GeoPandas point-in-polygon to cuSpatial (legacy 25.04 only)

cuSpatial is archived and incompatible with current RAPIDS packages. Use this only in an isolated environment pinned to 25.04. point_in_polygon returns a boolean membership matrix; it is not a drop-in replacement for geopandas.sjoin.

# Before (CPU)
import geopandas as gpd
import numpy as np
from shapely.geometry import Point

points = gpd.GeoSeries([Point(x, y) for x, y in coords], crs="EPSG:4326")
polygons = gpd.read_file("regions.geojson").geometry.iloc[:31]
polygons = polygons.to_crs(points.crs)
if any(points.intersects(polygon.boundary).any() for polygon in polygons):
    raise ValueError("Validate a boundary predicate explicitly before this port")
membership_cpu = np.column_stack(
    [points.within(polygon).to_numpy() for polygon in polygons]
)

# After (GPU, legacy) — point-by-polygon membership on these non-boundary inputs
import cuspatial

points_gpu = cuspatial.from_geopandas(points)
polygons_gpu = cuspatial.from_geopandas(polygons)
membership_gpu = cuspatial.point_in_polygon(points_gpu, polygons_gpu)
Raw lines 233 to 261
262### Exact Faiss search to exact cuVS search
264Match algorithmic semantics before benchmarking. Use cuVS brute force for an exact Faiss
265`IndexFlatL2` baseline; use CAGRA only when approximate results are acceptable and report recall@k
266against this exact ground truth.
268```python
269# Before (CPU) — Faiss
270import faiss
271import numpy as np
273rng = np.random.default_rng(42)
274embeddings = rng.random((1_000_000, 128), dtype=np.float32)
275queries = rng.random((1_000, 128), dtype=np.float32)
276index = faiss.IndexFlatL2(128)
277index.add(embeddings)
278distances, neighbors = index.search(queries, k=10)
280# After (GPU) — cuVS exact brute-force search
281import cupy as cp
282from cuvs.neighbors import brute_force
284embeddings_gpu = cp.asarray(embeddings)
285queries_gpu = cp.asarray(queries)
286index_gpu = brute_force.build(embeddings_gpu, metric="sqeuclidean")
287distances_gpu, neighbors_gpu = brute_force.search(index_gpu, queries_gpu, k=10)
288```

Exact Faiss search to exact cuVS search

Match algorithmic semantics before benchmarking. Use cuVS brute force for an exact Faiss IndexFlatL2 baseline; use CAGRA only when approximate results are acceptable and report recall@k against this exact ground truth.

# Before (CPU) — Faiss
import faiss
import numpy as np

rng = np.random.default_rng(42)
embeddings = rng.random((1_000_000, 128), dtype=np.float32)
queries = rng.random((1_000, 128), dtype=np.float32)
index = faiss.IndexFlatL2(128)
index.add(embeddings)
distances, neighbors = index.search(queries, k=10)

# After (GPU) — cuVS exact brute-force search
import cupy as cp
from cuvs.neighbors import brute_force

embeddings_gpu = cp.asarray(embeddings)
queries_gpu = cp.asarray(queries)
index_gpu = brute_force.build(embeddings_gpu, metric="sqeuclidean")
distances_gpu, neighbors_gpu = brute_force.search(index_gpu, queries_gpu, k=10)
Raw lines 262 to 289
290### scipy.sparse.linalg to RAFT
291```python
292# Before (CPU)
293import numpy as np
294from scipy.sparse import random as sparse_random
295from scipy.sparse.linalg import eigsh
297A = sparse_random(10000, 10000, density=0.01, format="csr", dtype=np.float32)
298A = A + A.T # Make symmetric
299eigenvalues, eigenvectors = eigsh(A, k=10, which="LM")
301# After (GPU) — RAFT sparse eigensolver
302import cupy as cp
303import cupyx.scipy.sparse as sp_gpu
304from pylibraft.sparse.linalg import eigsh as gpu_eigsh
306A_gpu = sp_gpu.csr_matrix(A) # Transfer to GPU
307eigenvalues, eigenvectors = gpu_eigsh(A_gpu, k=10, which="LM")
308```

scipy.sparse.linalg to RAFT

# Before (CPU)
import numpy as np
from scipy.sparse import random as sparse_random
from scipy.sparse.linalg import eigsh

A = sparse_random(10000, 10000, density=0.01, format="csr", dtype=np.float32)
A = A + A.T  # Make symmetric
eigenvalues, eigenvectors = eigsh(A, k=10, which="LM")

# After (GPU) — RAFT sparse eigensolver
import cupy as cp
import cupyx.scipy.sparse as sp_gpu
from pylibraft.sparse.linalg import eigsh as gpu_eigsh

A_gpu = sp_gpu.csr_matrix(A)  # Transfer to GPU
eigenvalues, eigenvectors = gpu_eigsh(A_gpu, k=10, which="LM")
Raw lines 290 to 308