Overview

2 Your first parallel program

This chapter introduces CUDA by building a first working parallel program and using a familiar grading-room analogy to explain how GPU work is divided among many threads. It starts with a minimal “Hello, Parallel World!” example that launches eight threads, each running the same kernel code, and then expands that idea into a real task: squaring the elements of an array. Along the way, it emphasizes that CUDA programs split responsibilities between the host CPU and the device GPU, and that kernels are the functions executed in parallel on the GPU.

The key programming ideas are simple but essential: mark GPU functions with __global__, launch them with the special <<<blocks, threads>>> syntax, and use threadIdx.x so each thread knows which piece of data belongs to it. Because thread execution order is not guaranteed, each thread must work independently on its own index, and bounds checks like if (idx < n) prevent out-of-range memory access. The chapter also explains how the hardware actually runs CUDA work in layers: threads are grouped into warps, warps run on streaming multiprocessors, and the GPU contains many CUDA cores that perform the arithmetic.

To make the array-squaring program work end to end, the chapter lays out the standard five-step CUDA workflow: allocate device memory, copy data from host to device, launch the kernel, synchronize so the CPU waits for the GPU, copy the results back, and free the GPU memory. It stresses that host and device have separate memories, so data must be moved explicitly with cudaMemcpy, and that forgetting synchronization, using the wrong copy direction, or missing bounds checks are the most common beginner mistakes. Overall, the chapter gives you the mental model and code pattern you need for simple data-parallel GPU programs.

Launch, then wait. The CPU fires the kernel and keeps going while the threads fan out and run in parallel. Threads finish at different times; cudaDeviceSynchronize() makes the CPU wait until the last thread finishes.
1D thread layout: one index per thread. In a 1D block, each thread gets a unique threadIdx.x starting from 0; that's all it needs to know which element to work on. (These numbers are unique within a block; with multiple blocks, each block counts from 0 again.) The six threads shown here are what a one-dimensional launch like kernel<<<1, 6>>>() creates: the launch is what brings the threads into being, and each one then reads its own threadIdx.x.
2D thread layout: two coordinates, one thread. In a 2D block, each thread is identified by a (threadIdx.y, threadIdx.x) pair (row, column), making it a natural fit for working with grids, images, or matrices. A block like this comes from a 2D launch, where you pass the block shape as a dim3 instead of the single number a 1D launch uses: here dim3 block(5, 4); kernel<<<1, block>>>(); for 5 columns (x) and 4 rows (y).
3D thread layout: three coordinates, one thread. In a 3D block, each thread is identified by a (threadIdx.z, threadIdx.y, threadIdx.x) triple (depth, row, column), making it a natural fit for working with volumetric data like 3D simulations or video frames. The same idea extends the launch: you pass a 3D dim3, for example dim3 block(4, 2, 2); kernel<<<1, block>>>(); for 4 columns (x), 2 rows (y), and 2 layers (z).
The containment hierarchy and where it runs. A kernel launch creates a grid; the grid contains blocks; each block’s threads are packed into warps of 32; and each block is assigned to an SM, whose cores execute those warps. Software on the left, hardware on the right.
The CUDA data lifecycle, five steps from CPU to GPU and back: allocate GPU memory, copy data in, run the kernel, copy results out, and free the memory.
Step 1 — Allocate: reserving space on the GPU. cudaMalloc carves out a block of device memory the same size as the host array, but doesn't copy anything yet. The GPU slots contain uninitialized data (the ? marks) until the copy step. Each region is labeled with the pointer that refers to it: h_data for the host array and d_data for the device array, a reminder that they point into two separate memories.
Step 2 — Copy In: moving data from host to device. cudaMemcpy transfers the array from CPU memory to the GPU: both sides now hold the same values, but from this point on, the GPU works with its own copy.
Step 3 — Compute: the GPU takes over. The CPU launches the kernel and steps back to wait; all four threads fire at once, each one reading its value from device memory, squaring it, and writing the result back (the up and down arrows), all in parallel. When the last thread finishes, cudaDeviceSynchronize() returns and the CPU carries on.
Step 4 — Copy Out: bringing the results back to the host. cudaMemcpy transfers the computed results from device to host: the squared values (9, 25, 4, 49) are now back on the CPU, ready to be used.
Step 5 — Free: clearing the desks out of the room. cudaFree returns the device memory to the GPU pool now that the results are safely on the host.

Summary

  • __global__ marks a function as a kernel that runs on many GPU threads in parallel.
  • <<<1, n>>> launches a kernel with n threads in one block. Each thread executes the same code independently.
  • A kernel launch is asynchronous: the CPU moves on immediately, so call cudaDeviceSynchronize() before you rely on the GPU’s results.
  • threadIdx.x gives each thread a unique index from 0 to n−1, its “desk number” for finding its own work.
  • The hardware runs threads in warps of 32 on its SMs, and the CUDA cores inside an SM do the math. You launch threads; the hardware does the packing.
  • The 5-step pattern: allocate GPU memory, send data in, launch kernel (and synchronize), retrieve results, free memory. Remember the analogy: Set Up Desks, Carry Exams, Grade, Collect, Clear. Almost every CUDA program that processes data follows it.
  • Bounds check: if (idx < n) prevents threads from accessing memory they don’t own. Make it a habit.
  • Data parallelism: the same operation applied to different data elements in parallel. Every kernel in this chapter is one instance of it, and the patterns in Part 2 are built on it.

FAQ

What is a CUDA kernel in this chapter?A kernel is a function marked with __global__ that runs on the GPU across many threads at the same time. Every thread executes the same kernel code, but each thread can work on different data.
Why do CUDA kernel functions use void as the return type?A kernel is launched by many threads, so there is no single return value for the CPU to receive. Instead, threads write results into GPU memory, and the CPU reads them back later.
What does the launch syntax <<<blocks, threads>>> mean?It tells CUDA how many blocks to launch and how many threads to put in each block. In this chapter, <<<1, 8>>> means one block with eight threads, which is treated as “launch 8 threads.”
Why must you call cudaDeviceSynchronize() after launching a kernel?Kernel launches are asynchronous, so the CPU does not wait automatically. cudaDeviceSynchronize() forces the CPU to wait until the GPU finishes, which is necessary before the program ends or before the host needs the results.
What does threadIdx.x represent?It is the thread’s index within its block. In this chapter’s 1D examples, each thread uses threadIdx.x to know which element of the array it should work on.
Why do we use threadIdx.x instead of just threadIdx?threadIdx is a 3D structure with .x, .y, and .z components. In a 1D layout, only threadIdx.x is needed.
Why is an if (idx < n) bounds check needed in the array kernel?Because you may launch more threads than there are array elements. The check prevents extra threads from reading or writing past the end of the array, which could cause errors or corrupted results.
Why do CUDA programs need both host memory and device memory?The CPU and GPU have separate memory spaces. Data starts in host memory, must be copied to device memory before the kernel can use it, and then copied back to host memory afterward.
What are the five steps in the GPU data lifecycle?The pattern is: allocate device memory, copy data from host to device, launch the kernel, copy results from device to host, and free device memory.
What is the difference between SPMD and SIMT?SPMD means the way you write the program: one program launched across many threads, each handling its own data. SIMT describes how NVIDIA hardware runs those threads: groups of 32 threads called warps execute the same instruction together.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Grokking Parallel Programming ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Grokking Parallel Programming ebook for free