Skip to content
Fenriar Tech
Go back

CUDA Programming (5): Program Timing and Performance Metrics

Note: This article is an English translation and adaptation of the original post published on SeanWangJS.github.io.

In high-performance computing, execution time and hardware utilization are the primary measures of software quality. CUDA provides specialized runtime APIs to capture kernel execution intervals on the Device with microsecond precision. In this article, we explore how to measure kernel execution time using CUDA Events, define essential performance metrics (effective memory throughput, compute throughput, and arithmetic intensity), and evaluate performance bottlenecks using the Roofline Model.


1. Kernel Timing with CUDA Events

While standard CPU timers (such as std::chrono) can measure end-to-end execution, they include CPU-GPU synchronization overhead and driver launch latency. To accurately isolate the execution time of a kernel on the Device, CUDA provides CUDA Events [1].

The following code illustrates how to profile a kernel using CUDA Events:

void vectorAdd(const float *a,
               const float *b,
               const int n,
               float *c) {
    
    float *d_a, *d_b, *d_c;
    cudaMalloc((void**)&d_a, n * sizeof(float));
    cudaMalloc((void**)&d_b, n * sizeof(float));
    cudaMalloc((void**)&d_c, n * sizeof(float));

    cudaMemcpy(d_a, a, n * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, b, n * sizeof(float), cudaMemcpyHostToDevice);

    int block_size = BLOCK_SIZE;
    int grid_size = (n + block_size - 1) / block_size;

    // Declare start and stop events
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    // Record start event into the default stream
    cudaEventRecord(start);

    // Launch kernel
    vector_add_kernel<<<grid_size, block_size>>>(d_a, d_b, n, d_c);

    // Record stop event
    cudaEventRecord(stop);

    // Wait until the stop event has been recorded by the GPU
    cudaEventSynchronize(stop);

    // Compute elapsed time in milliseconds
    float time = 0.0f;
    cudaEventElapsedTime(&time, start, stop);
    printf("Kernel Execution Time: %f ms\n", time);

    cudaMemcpy(c, d_c, n * sizeof(float), cudaMemcpyDeviceToHost);

    // Destroy events to free runtime resources
    cudaEventDestroy(start);
    cudaEventDestroy(stop);

    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
}

Key Timing Mechanics

  1. Event Creation and Recording: Events are created via cudaEventCreate and queued into the execution stream using cudaEventRecord. Because kernel launches are asynchronous, cudaEventRecord(start) and cudaEventRecord(stop) bracket the kernel in the stream.
  2. Event Synchronization: cudaEventSynchronize(stop) blocks the Host thread until the stop event is reached and executed by the GPU, ensuring that all intervening kernel operations have completed.
  3. Resolution: cudaEventElapsedTime calculates the time between two recorded events in milliseconds (ms), offering a sub-microsecond resolution (~0.5 µs) independent of CPU clock jitter.

2. Memory Throughput

Once execution time is measured, we can calculate Effective Memory Throughput to assess how efficiently a kernel utilizes memory bandwidth:

T=NtT = \frac{N}{t}

Where NN represents the total number of bytes read and written during execution, and tt is the kernel execution time in seconds. Throughput is typically expressed in GB/s.

For example, in vector addition with n=225n = 2^{25} (33,554,43233{,}554{,}432) elements:

If the kernel executes in t=2 mst = 2\text{ ms} (0.002 s0.002\text{ s}), the effective throughput is:

Throughput=0.375 GB0.002 s=187.5 GB/s\text{Throughput} = \frac{0.375\text{ GB}}{0.002\text{ s}} = 187.5\text{ GB/s}

Comparing this value against the GPU’s theoretical peak bandwidth (e.g., 1555 GB/s1555\text{ GB/s} on an NVIDIA A100) quantifies the memory bus efficiency of the implementation.


3. Compute Throughput

Complementing memory throughput, Compute Throughput measures the rate of floating-point operations executed per second, expressed in FLOPS (or GFLOPS / TFLOPS):

Compute Throughput=Total FLOPst\text{Compute Throughput} = \frac{\text{Total FLOPs}}{t}

By comparing achieved compute throughput against the theoretical peak compute performance derived in Part 1 (19.5 TFLOPS19.5\text{ TFLOPS} for A100 FP32), we determine how effectively the streaming multiprocessors (CUDA Cores) are kept occupied.


4. Arithmetic Intensity

Arithmetic Intensity (AI) is an intrinsic characteristic of an algorithm, defined as the ratio of floating-point operations performed to bytes of data accessed from global memory:

AI=FLOPsBytes Transferred(FLOP/Byte)\text{AI} = \frac{\text{FLOPs}}{\text{Bytes Transferred}} \quad (\text{FLOP/Byte})

Algorithms with high arithmetic intensity are computationally dense, while algorithms with low arithmetic intensity spend most of their time transferring data across the memory bus.

Example: Matrix Multiplication (n×nn \times n)

Consider a square matrix multiplication C=A×BC = A \times B:

The theoretical upper bound for the arithmetic intensity of matrix multiplication is therefore:

AI=2n3 FLOP12n2 Byte≈n6 FLOP/Byte\text{AI} = \frac{2n^3\text{ FLOP}}{12n^2\text{ Byte}} \approx \frac{n}{6}\text{ FLOP/Byte}

As matrix dimension nn scales up, arithmetic intensity increases linearly, transitioning the operation from memory-bound to compute-bound.


5. The Roofline Model

The Roofline Model [2][3] provides an intuitive visual framework for evaluating whether an application is constrained by memory bandwidth (memory-bound) or compute capacity (compute-bound) on a target hardware platform.

Mathematically, attainable performance is governed by two ceiling limits:

Pattainable=min⁡(Ppeak,AI×BWpeak)P_{\text{attainable}} = \min(P_{\text{peak}}, \text{AI} \times \text{BW}_{\text{peak}})

Where:

The turning point (or “knee” of the roofline) occurs at:

AIknee=PpeakBWpeak=19,500 GFLOPS1555 GB/s≈12.54 FLOP/Byte\text{AI}_{\text{knee}} = \frac{P_{\text{peak}}}{\text{BW}_{\text{peak}}} = \frac{19{,}500\text{ GFLOPS}}{1555\text{ GB/s}} \approx 12.54\text{ FLOP/Byte}

Figure 5.1: The Roofline Model Figure 5.1: The Roofline Model [3]

Interpreting the Model


References


Share this post:

Previous Post
CUDA Programming (4): Building CUDA Projects with CMake and GoogleTest
Next Post
CUDA Programming (6): Shared Memory and Matrix Multiplication Optimization