CUDA & the Modern GPU Stack
Session 12 • 2311CSC501J — Parallel Processing
What You'll Learn
- The CUDA model: host, device, kernels, launch config
- The thread hierarchy: thread → block → grid
- Vector add, reduction (Lab 5), image processing (Lab 3)
- How modern AI is served: Kubernetes, TensorRT, CUDA-X
"The GPU has thousands of cores. CUDA is how you finally get to command all of them by hand."
— the payoff of Unit IV
The CUDA Model: Host and Device
CUDA is C with a few extras that let the CPU hand work to the GPU. Two worlds, connected by a bus:
Host = the CPU
Runs main(), owns normal RAM, orchestrates: allocates GPU memory, copies data, launches kernels.
Device = the GPU
Runs kernels across thousands of threads at once, owns its own separate memory. Great at data parallelism (Session 06).
A kernel is a function marked __global__ that runs on the GPU. You launch it with a special syntax that says how many threads to spawn:
saxpy<<< blocks, threadsPerBlock >>>(n, a, d_x, d_y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the launch configuration
// "run this kernel on (blocks x threadsPerBlock) threads"
Thread → Block → Grid
Threads are organized in a three-level hierarchy. Every thread knows its own coordinates and uses them to find its slice of the data:
GRID (all the threads a kernel launches)
+-----------------------------------------------+
| BLOCK 0 BLOCK 1 BLOCK 2 |
| [t0 t1 t2 t3] [t0 t1 t2 t3] [t0 ...] | threadIdx.x = 0..3
+-----------------------------------------------+
blockIdx.x=0 blockIdx.x=1 blockIdx.x=2 blockDim.x = 4
global index of a thread:
i = blockIdx.x * blockDim.x + threadIdx.x
e.g. block 2, thread 1 -> 2 * 4 + 1 = 9
threadIdx
This thread's position inside its block.
blockIdx
Which block this thread belongs to.
blockDim
How many threads per block (the block's size).
The demo 04-cuda-threads.html lets you click a thread and watch this exact formula light up.
Memory: Two Separate Worlds
The GPU cannot read your normal C arrays. Host memory and device memory are separate. Moving data across the PCIe bus is the real cost of GPU programming — so you do it as little as possible.
Every CUDA program follows the same five steps
1. cudaMalloc(&d_x, bytes); // allocate on the DEVICE
2. cudaMemcpy(d_x, h_x, bytes, H2D); // copy inputs HOST -> DEVICE
3. kernel<<<blocks, threads>>>(d_x, ...); // launch: one thread per element
4. cudaMemcpy(h_y, d_y, bytes, D2H); // copy result DEVICE -> HOST
5. cudaFree(d_x); // free device memory
H2D = cudaMemcpyHostToDevice, D2H = cudaMemcpyDeviceToHost. Learn these five steps once and every kernel in this session is just step 3 getting cleverer.
Vector Add / SAXPY — CUDA's "Hello World"
y[i] = a*x[i] + y[i] for a million elements. Every element is independent, so we give one thread to one element — the CPU's loop disappears:
__global__ void saxpy(int n, float a, const float *x, float *y) {
// this thread's unique global index
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) { // bounds check (next slide)
y[i] = a * x[i] + y[i];
}
}
// launch: enough blocks of 256 threads to cover all n elements
int threads = 256;
int blocks = (n + threads - 1) / threads; // round UP
saxpy<<<blocks, threads>>>(n, 2.0f, d_x, d_y);
No for loop over the array. The loop is replaced by launching a million threads, each doing exactly one multiply-add. That is the SIMT model of Session 06 in code.
Why if (i < n) Matters
Threads come in whole blocks. If n = 1000 and a block holds 256 threads, you need 4 blocks — but 4 × 256 = 1024 threads for 1000 elements. The extra 24 must do nothing.
blocks = (1000 + 256 - 1) / 256 = 4 // ceiling division, rounds UP
threads launched = 4 * 256 = 1024
elements = 1000
^^^^^^^^ threads 1000..1023 run off the end!
if (i < n) // <- this skips them. Without it, they write
// past the array and corrupt GPU memory.
Without the check
Out-of-range threads read/write memory that isn't theirs — garbage results or a crash.
With the check
Extra threads quietly do nothing. Correct every time. It's one line — never skip it.
Watch: CUDA in a Nutshell
A fast, high-energy tour of what CUDA is and why the GPU changed computing. Great warm-up before the code.
CUDA (2007) opened the GPU — built for graphics — to general-purpose parallel computing. Every AI model you use today was trained through this door.
Inside the GPU: Three Kinds of Memory
Getting speed out of a GPU is mostly about which memory you use. Closer = faster = smaller.
| Memory | Shared by | Speed | Use it for |
|---|---|---|---|
| Registers | one thread | fastest | a thread's own local variables |
| Shared | all threads in a block | ~100× faster than global | threads in a block cooperating (reduction!) |
| Global | every thread + the host | slowest (but huge) | the big input/output arrays (cudaMalloc) |
The reduction on the next slide loads data once from slow global memory into fast shared memory, does all its back-and-forth there, and writes back once. That's the whole trick.
Lab 5 — Parallel Reduction on the GPU
"Sum a million numbers into one" is not embarrassingly parallel — the threads must cooperate. The classic answer is the tree reduction: halve the active threads each step.
step 0: [3][1][7][0][4][1][6][3] 8 values, 4 threads active
step 1: [7][2][13][3] . . . . add partner, halve (stride 4)
step 2: [20][5] . . . . . . . halve again (stride 2)
step 3: [25] . . . . . . . . one value = block's sum (stride 1)
for (int stride = blockDim.x/2; stride > 0; stride >>= 1) {
if (tid < stride) sdata[tid] += sdata[tid + stride];
__syncthreads(); // all threads finish this level first
}
A block of 256 finishes in log₂(256) = 8 steps, not 256. __syncthreads() is the barrier that keeps every thread in step. Full code in 02-reduction.cu.
Lab 3 — Image Processing: One Thread per Pixel
Every output pixel is computed the same way and is independent — the ultimate data-parallel shard. A 512×512 image becomes 262,144 threads. Because images are 2D, we use a 2D grid:
__global__ void to_grayscale(const unsigned char *rgb,
unsigned char *gray, int w, int h) {
int col = blockIdx.x * blockDim.x + threadIdx.x; // x direction
int row = blockIdx.y * blockDim.y + threadIdx.y; // y direction
if (col < w && row < h) {
int p = row * w + col;
gray[p] = 0.299f*rgb[p*3] + 0.587f*rgb[p*3+1] + 0.114f*rgb[p*3+2];
}
}
dim3 threads(16, 16); // 16x16 = 256 threads per block
dim3 blocks((w+15)/16, (h+15)/16); // enough blocks to cover the image
Same index math as vector add — just an x and a y. This is why GPUs dominate graphics, video, and computer vision. Full code in 03-image-grayscale.cu.
The Modern GPU Stack: From Kernel to Data Center
You just wrote a kernel by hand. In production, layers on top of CUDA turn one GPU into thousands serving AI at scale:
Kubernetes
Schedules containerized GPU workloads across a cluster — which job runs on which GPU, batch training, autoscaling. The orchestration layer.
TensorRT
NVIDIA's inference optimizer: takes a trained model and makes it small and fast to serve (fuses layers, lowers precision). "Make the model cheap to run."
CUDA-X AI
The library stack you build on instead of raw kernels: cuDNN (deep learning), cuBLAS (linear algebra), RAPIDS (data science).
Almost nobody hand-writes reduction kernels in production — they call cuBLAS/cuDNN, which are hand-tuned versions of exactly what you just learned. Knowing the kernel underneath is what makes you able to reason about the whole stack.
Unit IV Wrap-Up: Three Tools, One Idea
Unit IV gave you the three programming models that run essentially all parallel code today:
| Tool | Runs on | Unit of work | Use when |
|---|---|---|---|
| OpenMP (S10) | one shared-memory machine | threads | "use all the cores on this box" |
| MPI (S11) | many machines / a cluster | processes (ranks) | "scale past one box" |
| CUDA (S12) | an NVIDIA GPU | thousands of GPU threads | "massive, regular data parallelism" |
Different syntax, one idea: split the work, run the pieces at once, combine the results. Real systems mix all three (hybrid MPI + OpenMP + CUDA on a GPU cluster).
Recap & What's Next
Key Takeaways
- Host vs device: the CPU orchestrates, the GPU runs kernels across thousands of threads.
- Thread → block → grid: every thread finds its data with
i = blockIdx.x*blockDim.x + threadIdx.x, guarded byif (i < n). - Five steps: malloc → copy up → launch → copy down → free. Data movement is the cost.
- Reduction (Lab 5) cooperates via shared memory; image processing (Lab 3) is one thread per pixel.
- The production stack — Kubernetes, TensorRT, CUDA-X — is built on exactly these kernels.
That completes Unit IV. You can now write real parallel code in all three models — shared memory (OpenMP), distributed (MPI), and the GPU (CUDA). The hard part is behind you.
Next: Unit V — Applications of Parallel Computing
Where all this power actually gets used: scientific computing, parallel databases, Big Data, and the frontier.