A CPU is a latency machine — a few powerful cores that rip through sequential tasks. A GPU is a throughput machine — thousands of modest cores that chew through data in parallel. CUDA is NVIDIA's programming model that lets you harness those cores from ordinary C/C++ code. You write a function once, and the hardware runs it across thousands of threads simultaneously.
The central abstraction is simple: instead of looping over data, you launch a grid of threads where every thread handles its own slice of the problem. The GPU scheduler maps those threads onto physical cores automatically. You think in parallel; the hardware executes in parallel.
A kernel is a function marked with __global__ that runs on the GPU. When you call it, you specify how many threads to launch using the <<<blocks, threads>>> syntax. Every thread executes the same code but knows its own identity via built-in variables like threadIdx and blockIdx, so each thread operates on different data.
This pattern — one program, many data — is called SIMT (Single Instruction, Multiple Threads). It descends from the classical SIMD model but adds per-thread branching flexibility.
CUDA organizes threads into a two-level hierarchy:
Blocks can be 1D, 2D, or 3D — a convenience that maps naturally to vectors, matrices, or volumes. The hardware doesn't care about dimensionality; it flattens everything internally.
The GPU doesn't schedule individual threads. It schedules warps — groups of 32 threads that execute in lockstep on a single Streaming Multiprocessor (SM). Every clock cycle, all 32 threads in a warp execute the same instruction.
When threads in a warp diverge at an if statement, the hardware serializes the paths: it runs one branch with some threads masked off, then the other. This is warp divergence, and it costs performance. The golden rule: keep threads within a warp on the same code path whenever possible.
Performance in CUDA is dominated not by compute but by memory access patterns. The hierarchy, from fastest to slowest:
Shared memory is the key tool. It lives on-chip, has roughly the same latency as L1 cache, and is explicitly managed by the programmer. Classic technique: threads in a block cooperatively load a tile of data from slow global memory into fast shared memory, synchronize with __syncthreads(), then compute on the tile at full speed.
Global memory is accessed in 32-byte or 128-byte transactions. When all 32 threads in a warp read consecutive addresses, the hardware merges — coalesces — those requests into a single wide transaction. Scattered reads waste bandwidth because the bus fetches full cache lines regardless. Aligning data structures and using Struct-of-Arrays instead of Array-of-
to be continued....