GPU Architecture and CUDA Programming
GPU Architecture and CUDA Programming
~150-300 GB/sec
(high end GPUs)
Memory
DDR5 DRAM
(~1 GB)
GPU
Multi-core chip
SIMD execution within a single core (many execution units performing the same instruction)
Multi-threaded execution on a single core (multiple threads executed concurrently by a core)
CMU 15-418/618, Spring 2018
Graphics 101 + GPU history
(for fun)
3
1
4
2
Vertices Primitives
(points in space) (e.g., triangles, points, lines)
3
1
4
2
Vertices Primitives
(points in space) (e.g., triangles, points, lines)
Vertex Generation
Input: a list of vertices in 3D space
(and their connectivity into primitives) 3D vertex stream
list_of_positions = {
v0x, v0y, v0z,
v1x, v1y, v1x, triangle 0 = {v0, v1, v2}
v2x, v2y, v2z, triangle 1 = {v1, v2, v3}
v3x, v3y, v3x
};
Vertex Generation
Step 1: given a scene camera position,
compute where the vertices lie on screen 3D vertex stream
v2 Vertex Processing
v3
v1 Projected vertex
stream
v0
v2
v3
v1
v0
Vertex Generation
Step 2: group vertices into primitives
3D vertex stream
v2
v3 Vertex Processing
v1
Projected vertex
stream
v0
Primitive Generation
Primitive stream
t0 t1
Vertex Generation
Step 3: generate one fragment for each pixel a
primitive overlaps 3D vertex stream
Vertex Processing
Projected vertex
t0 stream
t1
Primitive Generation
Primitive stream
Fragment Generation
(“Rasterization”)
Fragment stream
Vertex Generation
Step 4: compute color of primitive for each
fragment (based on scene lighting and 3D vertex stream
Projected vertex
stream
Primitive Generation
Primitive stream
Fragment Generation
(“Rasterization”)
Fragment stream
Fragment Processing
Colored fragment
stream
Vertex Generation
Step 5: put color of the “closest fragment”
to the camera in the output image 3D vertex stream
Vertex Processing
Projected vertex
stream
Primitive Generation
Primitive stream
Fragment Generation
(“Rasterization”)
Fragment stream
Fragment Processing
Colored fragment
stream
Output image
buffer
(pixels) Pixel Operations
Vertex Generation
Primitive Generation
Primitive stream
Fragment Generation
(“Rasterization”)
Fragment stream
Fragment Processing
Colored fragment
stream
Output image
buffer
(pixels) Pixel Operations
Images from Matusik et al. SIGGRAPH 2003 CMU 15-418/618, Spring 2018
Early graphics programming (OpenGL API)
▪ Graphics programming APIs provided programmer mechanisms
to set parameters of scene lights and materials
Primitive Generation
Primitive stream
▪ Programmer provides mini-programs (“shaders”) Fragment Generation
that define pipeline logic for certain stages (“Rasterization”)
Colored fragment
stream
Output image
buffer
(pixels) Pixel Operations
void myFragmentShader()
{
vec3 kd = texture2D(myTexture, uv);
“fragment shader”
kd *= clamp(dot(lightDir, norm), 0.0, 1.0);
(a.k.a kernel function mapped onto
return vec4(kd, 1.0); input fragment stream)
}
We now can use the GPU like a data-parallel v3=(0, 512) v2=(512, 512)
programming system.
Hack!
v0=(0,0) v1=(512,0)
float scale_amount;
float input_stream<1000>; // stream declaration
float output_stream<1000>; // stream declaration
Fragment Processing
This was the only interface to GPU hardware.
GPU hardware could only execute graphics Output
image buffer
pipeline computations. (pixels)
Pixel Operations
// kernel definition
__global__ void matrixAddDoubleB(float A[Ny][Nx],
“Device” code (SPMD execution on GPU) float B[Ny][Nx],
float C[Ny][Nx])
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
// populate host address space pointer A What does cudaMemcpy remind you of?
for (int i=0 i<N; i++)
A[i] = (float)i;
// populate deviceA
cudaMemcpy(deviceA, A, bytes, cudaMemcpyHostToDevice);
Per-block
Readable/ writable by shared memory
all threads in block
Device global
memory
Readable/writable
by all threads
input[0] input[1] input[2] input[3] input[4] input[5] input[6] input[7] input[8] input[9]
CUDA Kernel
#define THREADS_PER_BLK 128
Host code
int N = 1024 * 1024
cudaMalloc(&devInput, sizeof(float) * (N+2) ); // allocate array in device memory
cudaMalloc(&devOutput, sizeof(float) * N); // allocate array in device memory
▪ Atomic operations
- e.g., float atomicAdd(float* addr, float amount)
- Atomic operations on both global memory and shared memory variables
▪ Host/device synchronization
- Implicit barrier across all threads at return of kernel
__syncthreads();
Will running this CUDA program
float result = 0.0f; // thread-local variable
for (int i=0; i<3; i++) create 1 million instances of
result += support[threadIdx.x + i];
local variables/stack?
output[index] = result / 3.f;
}
Mid-range GPU
(6 cores)
output[index] = result;
}
Decomposition
Sub-problems
(aka “tasks”, “work”)
Assignment
Worker Threads
Best practice: create enough workers to “fill” parallel machine, and no more:
- One worker per parallel execution resource (e.g., CPU core, core execution context)
- May want N workers per core (where N is large enough to hide memory/IO latency)
- Pre-allocate resources for each worker
- Dynamically assign tasks to worker threads (reuse allocation for many tasks)
Other examples:
- ISPC’s implementation of launching tasks
- Creates one pthread for each hyper-thread on CPU. Threads kept alive for remainder of program
- Thread pool in a web server
- Number of threads is a function of number of cores, not number of outstanding requests
- Threads spawned at web server launch, wait for work to arrive CMU 15-418/618, Spring 2018
NVIDIA GTX 980 (2014)
This is one NVIDIA Maxwell GM204 architecture SMM unit (one “core”)
Warp 0
Fetch/ Fetch/
Warp 1 Decode Decode
Warp 2 Warp Selector
...
Fetch/ Fetch/
Decode Decode
support[threadIdx.x] = input[index];
if (threadIdx.x < 2) {
support[THREADS_PER_BLK+threadIdx.x]
= input[index+THREADS_PER_BLK];
support }
(520 bytes)
__syncthreads();
output[index] = result;
Recall, CUDA kernels execute as SPMD programs }
On NVIDIA GPUs groups of 32 CUDA threads share an instruction stream. These groups called “warps”.
A convolve thread block is executed by 4 warps (4 warps x 32 threads/warp = 128 CUDA threads per block)
(Warps are an important GPU implementation detail, but not a CUDA abstraction!)
Shared (96 KB) Shared (96 KB) Shared (96 KB) Shared (96 KB)
... ... ... ...
... ...
Shared (96 KB) Shared (96 KB) Shared (96 KB) Shared (96 KB)
... ...
L2 Cache (2 MB)
224 GB/sec
(256 bit interface)
GPU memory
DDR5 DRAM
CMU 15-418/618, Spring 2018
NVIDIA GTX 980 (2014)
... ... ... ...
1.1 GHz clock
... ... ... ...
GPU memory
(DDR5 DRAM)
Let’s assume array size N is very large, so the host-side kernel launch generates thousands of thread blocks.
#define THREADS_PER_BLK 128
convolve<<<N/THREADS_PER_BLK, THREADS_PER_BLK>>>(N, input_array, output_array);
Fetch/Decode Fetch/Decode
Step 1: host sends CUDA device (GPU) a command (“execute this kernel”)
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
Step 2: scheduler maps block 0 to core 0 (reserves execution contexts for 128 threads and
520 bytes of shared storage)
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
Fetch/Decode Fetch/Decode
Block 0 (contexts 0-127) Block 0: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
Fetch/Decode Fetch/Decode
Block 0 (contexts 0-127) Block 0: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Block 2 (contexts 128-255)
Block 2: support
(520 bytes 0x520)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
Step 3: scheduler continues to map blocks to available execution contexts (interleaved mapping shown).
Only two thread blocks fit on a core
(third block won’t fit due to insufficient shared storage 3 x 520 bytes > 1.5 KB)
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Block 0 (contexts 0-127) Block 0: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Block 2 (contexts 128-255) Block 3 (contexts 128-255)
Block 2: support Block 3: support
(520 bytes 0x520) (520 bytes @ 0x520)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Block 4 (contexts 0-127) Block 4: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Block 2 (contexts 128-255) Block 3 (contexts 128-255)
Block 2: support Block 3: support
(520 bytes 0x520) (520 bytes @ 0x520)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Block 4 (contexts 0-127) Block 4: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Block 3 (contexts 128-255)
Block 3: support
(520 bytes @ 0x520)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Running the CUDA kernel
Kernel’s execution requirements:
Each thread block must execute 128 CUDA threads
Each thread block must allocate 130 x sizeof(float) = 520 bytes of shared memory
EXECUTE: convolve
ARGS: N, input_array, output_array
NUM_BLOCKS: 1000
Fetch/Decode Fetch/Decode
Block 4 (contexts 0-127) Block 4: support Block 1 (contexts 0-127) Block 1: support
(520 bytes @ 0x0) (520 bytes @ 0x0)
Block 5 (contexts 128-255) Block 3 (contexts 128-255)
Block 5: support Block 3: support
(520 bytes 0x520) (520 bytes @ 0x520)
Core 0 Core 1
CMU 15-418/618, Spring 2016
Review: what is a “warp”?
▪ A warp is a CUDA implementation detail on NVIDIA GPUs
▪ On modern NVIDIA hardware, groups of 32 CUDA threads in a thread block are
executed simultaneously using 32-wide SIMD execution.
Fetch/Decode
thread 0 ctx
… Warp 0 context
thread 31 ctx
In this fictitious NVIDIA GPU example:
thread 32 ctx Core maintains contexts for 12 warps
… Warp 1 context
thread 63 ctx
Selects one warp to run each clock
thread 64 ctx
…
support[threadIdx.x] = input[index];
Warp 2 if (threadIdx.x < 2) {
support[THREADS_PER_BLK+threadIdx.x]
= input[index+THREADS_PER_BLK];
“Shared” memory }
Warp 3 (96 KB)
__syncthreads();
L1 cache
float result = 0.0f; // thread-local
for (int i=0; i<3; i++)
result += support[threadIdx.x + i];
Imagine a thread block with 256 CUDA threads
(see code, top-right) }
output[index] = result;
▪ CUDA implementation:
- A Kepler GPU warp has performance characteristics akin to an ISPC gang of instances (but unlike
an ISPC gang, the warp concept does not exist in the programming model*)
- All warps in a thread block are scheduled onto the same core, allowing for high-BW/low latency
communication through shared memory variables
- When all threads in block complete, block resources (shared memory allocations, warp execution
contexts) become available for next block
* Exceptions to this statement include intra-warp builtin operations like swizzle and vote CMU 15-418/618, Spring 2018
Consider a program that creates a histogram:
▪ This example: build a histogram of values in an array
- All CUDA threads atomically update shared variables in global memory
▪ Notice I have never claimed CUDA thread blocks were guaranteed to be independent. I
only stated CUDA reserves the right to schedule them in any order.
▪ This is valid code! This use of atomics does not impact implementation’s ability to
schedule blocks in any order (atomics used for mutual exclusion, and nothing more)
Global memory
int counts[10]
...
int A[N]
int* A = {0, 3, 4, 1, 9 , 2, . . . , 8, 4 , 1 }; // array of integers between 0-9
CMU 15-418/618, Spring 2018
But is this reasonable CUDA code?
▪ Consider implementation of on a single core GPU with resources
for one CUDA thread block per core
- What happens if the CUDA implementation runs block 0 first?
- What happens if the CUDA implementation runs block 1 first?
Global memory
int myFlag
(assume myFlag is initialized to 0)
__syncthreads();
} Now programmer’s mental model is that
}
*all* threads are concurrently running on
// host code //////////////////////////////////////////////////////
int N = 1024 * 1024;
the machine at once.
cudaMalloc(&devInput, N+2); // allocate array in device memory
cudaMalloc(&devOutput, N); // allocate array in device memory
// properly initialize contents of devInput here ...
▪ Memory semantics
- Distributed address space: host/device memories
- Thread local/block shared/global variables within device memory
- Loads/stores move data between them (so it is correct to think about local/shared/
global memory as being distinct address spaces)