Unit IV — Programming Models & Tools

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
Registersone threadfastesta thread's own local variables
Sharedall threads in a block~100× faster than globalthreads in a block cooperating (reduction!)
Globalevery thread + the hostslowest (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 machinethreads"use all the cores on this box"
MPI (S11)many machines / a clusterprocesses (ranks)"scale past one box"
CUDA (S12)an NVIDIA GPUthousands 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

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.