00 > One Block, One Thread
This lesson runs the simplest possible CUDA program. Its kernel uses one block and one thread, with no parallelism, so you can see the first output before anything gets complex. Every later lesson changes this program a little.
[!NOTE] All outputs on this page come from an NVIDIA L40S with CUDA 13.0 on Ubuntu 24.
GPU vs CPU
On the CPU, a function runs once on one core. On the GPU, a kernel runs many times in parallel. A kernel is a function that runs on the GPU. Each running copy of it is called a thread. Two numbers set how many threads run. One is the number of blocks, the other is the number of threads per block.
For example, 2 blocks of 3 threads each start 2 x 3 = 6 threads. All 6 run the same kernel code.
What __global__ Means
__global__ void printIDs() { ... }
__global__ marks a function as a GPU kernel. The compiler builds it for the GPU, not the CPU. The CPU calls it, but it runs on the GPU.
[!NOTE] Two other qualifiers exist.
__device__runs on the GPU and can be called only from GPU code.__host__is a normal CPU function that can be called only from the CPU.
Launch Configuration <<<blocks, threads>>>
printIDs<<<1, 1>>>();
// ^ ^
// blocks -+ +- threads per block
The <<<...>>> syntax is the execution configuration. It goes between the function name and the argument list. The first number is the number of blocks. The second number is the threads per block. <<<1, 1>>> means one block with one thread. Total threads are 1 x 1 = 1.
Thread, Block, Grid
Every kernel launch creates three levels.
- A thread is the smallest unit. One thread runs one copy of the kernel.
- A block is a group of threads that runs on one SM. An SM is one of the many small processors inside a GPU. The threads of a block can share memory.
- A grid is all blocks of one kernel launch. One launch, one grid.
[!NOTE] A GPU has many SMs. The L40S used in these lessons has 142. A block never splits across two SMs, but different blocks can run on different SMs at the same time. Lesson 02 uses this.
blockIdx.x and threadIdx.x
printf("Block ID: %d Thread ID: %d", blockIdx.x, threadIdx.x);
blockIdx.x is the index of the block this thread is in. threadIdx.x is the index of this thread inside its block. Both start at 0. Both have .x, .y and .z parts, because grids and blocks can be 1D, 2D or 3D. For 1D work, you only use .x. With <<<1, 1>>>, both are always 0.
Header Files
cuda_runtime.his the CUDA runtime API. It declarescudaDeviceSynchronize()and the error-checking functions.stdio.his standard C, needed forprintf.device_launch_parameters.hmakesblockIdx,threadIdx,blockDimandgridDimknown to the editor when you use MSVC or certain IDEs.nvccdoes not need it, but it does no harm.
cudaDeviceSynchronize()
Kernel launches are asynchronous. The CPU starts the kernel and goes to the next line at once. Without cudaDeviceSynchronize(), main() returns and the program exits before the GPU prints anything. This function makes the CPU wait until all GPU work is done.
[!WARNING] If you forget
cudaDeviceSynchronize(), the program still compiles and runs without an error. It just prints nothing.
Code
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
__global__ void printIDs()
{
printf("\nBlock ID: %d === Thread ID: %d", blockIdx.x, threadIdx.x);
}
int main()
{
printIDs<<<1, 1>>>();
cudaDeviceSynchronize();
return 0;
}
- The three
#includelines load the headers described above. printIDsis the kernel. Each thread prints its block ID and its thread ID. The\nat the start of the string puts each line on a new line.printIDs<<<1, 1>>>();starts the kernel with one block of one thread.cudaDeviceSynchronize();waits for the GPU, so the print shows up before the program ends.
Code Walkthrough
Step through the program in the order you would write it from an empty file.
1-3 cpuHeaders. Type the three#includelines first, because everything below uses names they declare.cuda_runtime.hbrings incudaDeviceSynchronize()andstdio.hbrings inprintf. Withoutstdio.h, theprintfcall in the kernel fails to compile.5-6,8 gpuThe empty kernel. Write the kernel's outline before its body, with__global__, the return typevoid, a name and an empty pair of braces. A kernel must returnvoid, because it has no caller waiting for a value. If you leave out__global__, the compiler builds a normal CPU function and the launch line later fails to compile.7 gpuThe kernel body. Add oneprintfthat printsblockIdx.xandthreadIdx.x. This line runs once in every thread, on the GPU. Each%dis filled by the value listed after the string, in the same order.10-11,14-15 cpuThe main function. Writemainwith its braces andreturn 0;now, then fill the middle. This is normal C code that runs on the CPU.12 cpuThe launch. Call the kernel by its name, then<<<1, 1>>>, then the argument list(). The rule is blocks first, threads per block second. The empty()is still needed, even thoughprintIDstakes no arguments.13 cpuWait for the GPU. The launch returns at once, so addcudaDeviceSynchronize();right after it. This is the most common first mistake. Without it the program compiles, runs and prints nothing.
Compile and Run
The first command compiles the code into a program. The second command runs it.
nvcc -o first_kernel first_kernel.cu
./first_kernel
nvccis the CUDA compiler. It builds the CPU part and the GPU part of the file.-o first_kernelnames the programfirst_kernel. Without it the name isa.out.first_kernel.cuis the source file with the code above. CUDA source files end in.cu../first_kernelruns the program. The./tells the shell to look in the current folder.
Output
Block ID: 0 === Thread ID: 0
- There is one line because there is one thread, and each thread prints once.
- Both IDs are 0 because the only block and the only thread each get index 0.
- The output is the same on every run, because one thread has no other thread to race with.
Write It Yourself
Write a one-thread kernel from scratch that greets you from the GPU.
- Create a file
hello.cuwith the skeleton below. - Write the kernel
hello, which printsHello from block 0, thread 0usingblockIdx.xandthreadIdx.x. - Launch it with one block of one thread, and make the CPU wait for it.
#include "cuda_runtime.h"
#include <stdio.h>
// TODO: write the kernel hello() that prints its block ID and thread ID
int main()
{
// TODO: launch hello with 1 block of 1 thread
// TODO: wait for the GPU to finish
return 0;
}
Hint
A kernel starts with __global__ void. The launch looks like hello<<<1, 1>>>();, and the wait is cudaDeviceSynchronize();.
Solution
#include "cuda_runtime.h"
#include <stdio.h>
__global__ void hello()
{
printf("Hello from block %d, thread %d\n", blockIdx.x, threadIdx.x);
}
int main()
{
hello<<<1, 1>>>();
cudaDeviceSynchronize();
return 0;
}
Compile and run it with nvcc -o hello hello.cu and ./hello. You should see the line Hello from block 0, thread 0.
Glossary
- CUDA (Compute Unified Device Architecture): NVIDIA's platform for running your own code on the GPU.
- GPU (Graphics Processing Unit): the processor with thousands of small cores that runs kernels.
- CPU (Central Processing Unit): the main processor. It runs
main()and starts kernels. - kernel: a function that runs on the GPU. You write it once, and the GPU runs it on many threads at the same time.
- thread: the smallest unit of execution. One thread is one running copy of the kernel, with its own ID.
- block: a group of threads that runs on one SM. They can share data through shared memory.
- grid: all blocks started by one kernel call.
- SM (Streaming Multiprocessor): one of the processors inside the GPU. Blocks run on SMs.
__global__: tells the compiler this function is a GPU kernel. The CPU calls it and the GPU runs it.blockIdx.x: the index of the block the current thread is in. Starts at 0.threadIdx.x: the index of the current thread inside its block. Starts at 0.cudaDeviceSynchronize(): makes the CPU wait until the GPU finishes all its work.- asynchronous: the CPU does not wait. It sends a command to the GPU and moves on at once.
- API (Application Programming Interface): the set of functions a library offers, here the CUDA runtime functions.
- 3D (three-dimensional): a shape with
x,yandzsizes. 1D uses onlyx, 2D usesxandy. - compiler: the program that turns your source code into a program the machine can run. For CUDA it is
nvcc. - execution configuration: the
<<<blocks, threads>>>part of a kernel launch that sets how many threads run. - MSVC (Microsoft Visual C++): Microsoft's C and C++ compiler on Windows.
- IDE (Integrated Development Environment): an editor with build and debug tools, such as Visual Studio.