学习笔记

CUDA性能分析与优化逐元素访存

19 分钟阅读
学习笔记CUDA

先把上次没有写完的grid-stride的给写完。

#include <iostream>
#include <iomanip>
#include <vector>
#include <cmath>
#include <algorithm>
#include <cstdlib>
#include <cuda_runtime.h>

#define CUDA_CHECK(call) { \
    cudaError_t err = call ;   \
    if (err != cudaSuccess){ \
        std::cerr << "CUDA error at " << __FILE__ << ":" << __LINE__ << ": " \
                  << cudaGetErrorString(err) << '\n'; \
        exit(1); \
    }    \
} \


template <typename T>
__global__ void add_kernel(T *c, const T *a, const T *b, size_t n) {
    size_t idx = (blockDim.x) * blockIdx.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx] ;
    }
}

template <typename T> 
__global__ void add_grid_stride(T *c, const T *a, const T *b, size_t n){
    size_t first = static_cast<size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    size_t stride = static_cast<size_t>(blockDim.x) * gridDim.x;
    for (size_t i = first; i < n; i += stride) {
        c[i] = a[i] + b[i] ; 
    }
}

void free_device_memory(float *d_a, float *d_b, float *d_c) {
    if(d_a) CUDA_CHECK(cudaFree(d_a));
    if(d_b) CUDA_CHECK(cudaFree(d_b));
    if(d_c) CUDA_CHECK(cudaFree(d_c));
}

bool run_case(size_t n, unsigned int block_size, unsigned int grid_size){
    const size_t SIZE = n ;

    if (grid_size == 0) {
        grid_size = static_cast<unsigned int>(
            n / block_size + (n % block_size != 0)
        );
    }

    if (SIZE == 0) {
        std::cout << "Empty input: passed!\n";
        return true;
    }

    std::vector<float> h_a(SIZE, 1) ;
    std::vector<float> h_b(SIZE, 2);
    std::vector<float> h_c(SIZE, 0);
    std::vector<float> ref(SIZE);

    float *d_a, *d_b, *d_c;
    d_a = d_b = d_c = nullptr;

    for (size_t i = 0; i < SIZE; i ++){
        h_a[i] = static_cast<float>(i % 97) - 48.0f;
        h_b[i] = static_cast<float>(i % 31) * 0.25f;
        ref[i] = h_a[i] + h_b[i];
    }
     
    size_t size_bytes = SIZE * sizeof(float);

    CUDA_CHECK(cudaMalloc(&d_a, size_bytes));
    CUDA_CHECK(cudaMalloc(&d_b, size_bytes));
    CUDA_CHECK(cudaMalloc(&d_c, size_bytes));

    CUDA_CHECK(cudaMemcpy(d_a, h_a.data(), size_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_b, h_b.data(), size_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_c, h_c.data(), size_bytes, cudaMemcpyHostToDevice));
    
    unsigned int BLOCK_SIZE = block_size;
    dim3 block_dim(BLOCK_SIZE);
    dim3 grid_dim(grid_size) ;

    add_grid_stride<<<grid_dim,block_dim>>>(d_c, d_a, d_b, SIZE);
    
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    CUDA_CHECK(cudaMemcpy(h_c.data(), d_c, size_bytes, cudaMemcpyDeviceToHost));

    for (size_t i = 0; i < SIZE; i ++) {
        if (fabs(h_c[i] - ref[i]) > 1e-5){
            std::cerr <<  "Verification failed at index " << i << ": "
                        << h_c[i] << " != 3.0\n";
            free_device_memory(d_a, d_b, d_c);
            d_a = d_b = d_c = nullptr;
            printf("failed!\n");
            return false;
        }
    }

    free_device_memory(d_a, d_b, d_c);
    d_a = d_b = d_c = nullptr;
    printf("passed!\n"); 
    return true ;
}

int main () {
    const size_t test_sizes[] = {
        0, 1, 31, 32, 33, 255, 256, 257, 1000003
    };

    bool all_passed = true;

    std::cout << "===== Normal grid =====\n";

    for (size_t n : test_sizes) {
        bool passed = run_case(n, 256, 0);
        all_passed = passed && all_passed;
    }

    std::cout << "\n===== Fixed small grid =====\n";

    for (size_t n : test_sizes) {
        bool passed = run_case(n, 128, 2);
        all_passed = passed && all_passed;
    }

    std::cout << "\n===== Single thread =====\n";

    for (size_t n : test_sizes) {
        if (n > 257) {
            continue;
        }

        bool passed = run_case(n, 1, 1);
        all_passed = passed && all_passed;
    }

    return all_passed ? EXIT_SUCCESS : EXIT_FAILURE;
}

运行了一下这个Nsight Systems(CLI):
我先记录一下相关操作:

nvcc -std=c++17 -lineinfo vector_add_bench.cu -o vector_add_bench
nsys profile     -t cuda,nvtx,osrt     -o vector_add_bench     -f true     ./vector_add_bench
nsys stats vector_add_bench.nsys-rep --force-export=true



现在我先跑<<<1,1>>>:

然后跑<<<256,256>>>:

HtoD 和DtoH 的耗时均远大于核函数时间

从而:

试试nsight compute:

ncu   --print-details all   --nvtx   --call-stack   --set full   ./vector_add_bench


这里说明我们加法的性能瓶颈在内存访问上,因此想办法提升其访存效率。

现在我加入了向量化访存并行加法的方法,然后我记录一下结果:

对于float4:



DRAM Throughput = 89.29%
Compute Throughput = 6.27%
这说明 GPU大部分时间在搬数据,计算单元没有被充分使用,但这不是实现不好,而是向量加法本身的特征。

$Performance = \frac{1048576}{43.71 \times10^{-6}} \approx24.0\ \text{GFLOP/s}$

所以roofline的点在(0.0833 FLOP/byte, 24.0 GFLOP/s)

float4 向量加法每处理四个标量需要读取两个 float4、写入一个 float4,共移动48字节并执行4次浮点加法,因此计算强度仍为 4/48=1/12≈0.0833 FLOP/byte。向量化没有提高计算强度,而是减少了访存和地址计算指令。Nsight Compute 显示当前 kernel 的 DRAM Throughput 为89.29%,Compute Throughput 仅为6.27%,说明它是显存带宽受限算子。按43.71 μs计算,其有效性能约为24.0 GFLOP/s,已经接近当前带宽对应的 Roofline。

对于float2:



有效计算性能:
$\frac{1048576}{42.75\ \mu s}\approx24.53\ \text{GFLOP/s}$
float2 和 float4 都已经接近显存带宽 Roofline。float2 本次比 float4 快约2.25%,具有更高的 Waves/SM、更少的每线程寄存器,并取得略高的内存吞吐率。不过差距较小,需要 CUDA Event 多轮测量确认稳定性。


scalar、float2 和 float4 分别生成了32位、64位和128位全局访存指令,但三者算法计算强度均为 1/12≈0.0833 FLOP/byte,逻辑内存流量也完全相同。在 RTX 3060 Laptop GPU 上,三个版本的 DRAM Throughput 均达到约88%~90%,说明它们都受显存带宽限制并接近相同的 Roofline。本次 NCU 采集中 float2 用时42.75 μs,略快于 scalar 的43.14 μs和 float4 的43.71 μs,但最大差距只有约2.2%,需要通过预热后的 CUDA Event 多轮测量判断差异是否稳定。

其它

后面还有个半精度的东西,但我懒得搞了,差不多都学明白了,包括roofline的使用,nvcc查看性能等等,就酱!

我发现拿AI去辅助学习这些内容还真是狗屎呢,它完全不考虑你学习曲线的问题,然后还一直去纠结一些不必要的问题,我也是麻了。后面还是自己拿ppt的资料学吧。

还是把ppt的内容粘上来吧www:

#include <iostream>
#include <iomanip>
#include <vector>
#include <cmath>
#include <algorithm>
#include <cstdlib>
#include <cuda_runtime.h>
#include <cstdint> 
#include <type_traits>


#define CUDA_CHECK(call) { \
    cudaError_t err = call ;   \
    if (err != cudaSuccess){ \
        std::cerr << "CUDA error at " << __FILE__ << ":" << __LINE__ << ": " \
                  << cudaGetErrorString(err) << '\n'; \
        exit(1); \
    }    \
} \

template <typename T> 
__device__ T add(const T &a, const T &b){
    if constexpr (std::is_same_v<T, float>) {
        return a + b;
    }
    else if constexpr (std::is_same_v<T, float2>) {
        return make_float2(a.x + b.x, a.y + b.y);
    }
    else if constexpr (std::is_same_v<T, float4>) {
        return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
    }
}

template <typename T>
__global__ void add_kernel(T *c, const T *a, const T *b, size_t n, size_t step) {
    size_t idx = (blockDim.x) * blockIdx.x + threadIdx.x;
    for (size_t i = idx; i < n; i += step) {
        c[i] = add(a[i] , b[i]) ;
    }
}

template <typename T>
void vector_add(T *c, const T *a, const T *b, size_t n, const dim3 &grid, const dim3 &block){
    size_t step = grid.x * block.x;
    add_kernel<T><<<grid, block>>>(c, a, b, n, step);
}


void free_device_memory(float *d_a, float *d_b, float *d_c) {
    if(d_a) CUDA_CHECK(cudaFree(d_a));
    if(d_b) CUDA_CHECK(cudaFree(d_b));
    if(d_c) CUDA_CHECK(cudaFree(d_c));
}

bool run_case(size_t n, unsigned int block_size, unsigned int grid_size){
    const size_t SIZE = n ;

    if (grid_size == 0) {
        grid_size = static_cast<unsigned int>(
            n / block_size + (n % block_size != 0)
        );
    }

    if (SIZE == 0) {
        std::cout << "Empty input: passed!\n";
        return true;
    }

    std::vector<float> h_a(SIZE, 1) ;
    std::vector<float> h_b(SIZE, 2);
    std::vector<float> h_c(SIZE, 0);
    std::vector<float> ref(SIZE);

    float *d_a, *d_b, *d_c;
    d_a = d_b = d_c = nullptr;

    for (size_t i = 0; i < SIZE; i ++){
        h_a[i] = static_cast<float>(i % 97) - 48.0f;
        h_b[i] = static_cast<float>(i % 31) * 0.25f;
        ref[i] = h_a[i] + h_b[i];
    }
     
    size_t size_bytes = SIZE * sizeof(float);

    CUDA_CHECK(cudaMalloc(&d_a, size_bytes));
    CUDA_CHECK(cudaMalloc(&d_b, size_bytes));
    CUDA_CHECK(cudaMalloc(&d_c, size_bytes));

    CUDA_CHECK(cudaMemcpy(d_a, h_a.data(), size_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_b, h_b.data(), size_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_c, h_c.data(), size_bytes, cudaMemcpyHostToDevice));
    
    unsigned int BLOCK_SIZE = block_size;
    dim3 block_dim(BLOCK_SIZE);
    size_t vector_count = n / 2;
    size_t vector_elements = vector_count * 2;
    unsigned int vector_grid_size = static_cast<unsigned int>(vector_count / block_size + (vector_count % block_size != 0));
    dim3 grid_dim(vector_grid_size) ;

    vector_add<float2>(
        reinterpret_cast<float2*>(d_c),
        reinterpret_cast<const float2*>(d_a),
        reinterpret_cast<const float2*>(d_b),
        vector_count,
        grid_dim,
        block_dim
    );
    
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    CUDA_CHECK(cudaMemcpy(h_c.data(), d_c, size_bytes, cudaMemcpyDeviceToHost));

    for (size_t i = 0; i < SIZE; i ++) {
        if (fabs(h_c[i] - ref[i]) > 1e-5){
            std::cerr <<  "Verification failed at index " << i << ": "
                        << h_c[i] << " != 3.0\n";
            free_device_memory(d_a, d_b, d_c);
            d_a = d_b = d_c = nullptr;
            printf("failed!\n");
            return false;
        }
    }

    free_device_memory(d_a, d_b, d_c);
    d_a = d_b = d_c = nullptr;
    printf("passed!\n"); 
    return true ;
}

int main () {
    const size_t SIZE = 1 << 20 ;
    run_case(SIZE,256,256);
    
}