CUDA笔记002:CUDA基础入门

前言

本篇为了巩固基础的cuda语法,学习部分cuda的相关函数

使用资源

谭升的CUDA博客:2.0-2.4:https://face2ai.com/CUDA-F-2-0-CUDA编程模型概述1/

file:///F:/new_interview/%E7%9F%A5%E8%AF%86%E5%BA%93/cuda/cuda%E6%95%99%E7%A8%8B/Professional%20CUDA%20C%20Programming.pdf

谭升的CUDA博客

2.0

  • 每个block内部有自己独享的shared memory
  • grid内部有全局的global memory

一个经常会发生的错误就是混用设备和主机的内存地址!!

所以,为了避免混淆,提出了一个方案:给内存变量加后缀,_d表示device,_h表示host

不同block内线程不能相互影响!他们是物理隔离的!

计算thread index:依赖blockIdxthreadIdx

blockIdx.x
blockIdx.y
blockIdx.z
threadIdx.x
threadIdx.y
threadIdx.z

以上两个index的范围,由dim3类型的blockDimgridDim指定

<<<grid,block>>>

  • grid: grid中的block num
  • block: block中的thread num

2.1

一个很重要的思想是:把串行的for展开,然后可以并行化。

这个过程通常需要借助blockIdxthreadIdx,计算自己的thread_index,例如:

__global__ void sumArraysOnGPU(float *A, float *B, float *C) {
  int i = threadIdx.x;
  C[i] = A[i] + B[i];
}

Q: 一个编写良好(至少正确)的核函数,grid_num和block_num不会影响其正确性,<<<32, 32>>> 和 <<<1, 1>>>结果相同,只是执行时间有区别。这种说法正确吗?
A: 不完全正确,编程实践中是会依赖gridDim 和 blockDim的。

Q: 对于线程数量不足的情况,可以添加for使得该线程处理更多任务,而当线程够多时则每个线程处理自己的。这样的编程思想正确吗?
A: 正确的,这种思想被称为 “循环展开(loop unrolling)” 或 “任务批处理(work batching)”,是 CUDA 编程中常见的一种优化策略。尤其是在支持的最大线程数不足时使用。

所以,不必强求gridDim 和 blockDim即使变化也要兼容,只需要确定预先设置正确即可


注意,大部分cuda函数的返回类型都是cudaError_t ,看起来类似Linux中的errno
一个处理函数的小技巧:

#define CHECK(call)\
{\
  const cudaError_t error=call;\
  if(error!=cudaSuccess)\
  {\
      printf("ERROR: %s:%d,",__FILE__,__LINE__);\
      printf("code:%d,reason:%s\n",error,cudaGetErrorString(error));\
      exit(1);\
  }\
}

CHECK(cudaMemcpy(res_from_gpu_h,res_d,nByte,cudaMemcpyDeviceToHost));

2.2

并行程序中的函数计时:
clock()函数计算的是cpu time,在并行场景下计算的是所有thread的cpu time。所以并行场景下需要使用gettimeofday
否则你可能在计算某个函数的性能占比时,算出来超过100%,这个情况在我写trigger benchmark的时候真的遇到过。

在性能测试中发现:当数据不能被完整切块的时候性能滑铁卢了,这个我们可以使用一点小技巧,比如只传输可完整切割数据块,然后剩下的1,2个使用cpu计算

尽量使得线程数量和数据数量对齐,没必要折磨自己也折磨机器

nvprof是cuda的性能分析工具,做性能优化使用。

nvprof ./test 尝试运行vectorAdd,结果如下:

==54506== NVPROF is profiling process 54506, command: ./test
==54506== Warning: Unified Memory Profiling is not supported on the current configuration because a pair of devices without peer-to-peer support is detected on this multi-GPU setup. When peer mappings are not available, system falls back to using zero-copy memory. It can cause kernels, which access unified memory, to run slower. More details can be found at: http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#um-managed-memory
最大误差: 0
==54506== Profiling application: ./test
==54506== Warning: 26 API trace records have same start and end timestamps.
This can happen because of short execution duration of CUDA APIs and low timer resolution on the underlying operating system.
==54506== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   60.46%  2.4588ms         1  2.4588ms  2.4588ms  2.4588ms  [CUDA memcpy DtoH]
                   38.46%  1.5640ms         2  782.01us  681.73us  882.28us  [CUDA memcpy HtoD]
                    1.09%  44.192us         1  44.192us  44.192us  44.192us  add(float*, float*, float*, int)
      API calls:   92.08%  253.26ms         3  84.419ms  412.90us  249.43ms  cudaMalloc
                    3.78%  10.404ms         1  10.404ms  10.404ms  10.404ms  cudaLaunchKernel
                    2.06%  5.6755ms         3  1.8918ms  831.60us  3.9078ms  cudaMemcpy
                    1.79%  4.9199ms       114  43.157us       0ns  4.9073ms  cuDeviceGetAttribute
                    0.28%  759.50us         3  253.17us  181.90us  377.10us  cudaFree
                    0.00%  8.1000us         1  8.1000us  8.1000us  8.1000us  cuDeviceGetPCIBusId
                    0.00%  1.1000us         3     366ns     100ns     700ns  cuDeviceGetCount
                    0.00%  1.0000us         2     500ns     300ns     700ns  cuDeviceGet
                    0.00%  1.0000us         1  1.0000us  1.0000us  1.0000us  cuDeviceGetName
                    0.00%     200ns         1     200ns     200ns     200ns  cuDeviceTotalMem
                    0.00%     200ns         1     200ns     200ns     200ns  cuDeviceGetUuid
                    0.00%     100ns         1     100ns     100ns     100ns  cuModuleGetLoadingMode

观察到,实际的add只有1.09%,时间占比绝大多数是内存拷贝

以上是当前的数据性能,我们需要学会评估当前设备的理论计算极限,尽可能优化代码接近这个极限。

各个设备的理论极限可以通过其芯片说明计算得到,以下是谭升给出的一个计算实例:

Tesla K10 单精度峰值浮点数计算次数:745MHz核心频率 x 2GPU/芯片 x(8个多处理器 x 192个浮点计算单元 x 32 核心/多处理器) x 2 OPS/周期 =4.58 TFLOPS
Tesla K10 内存带宽峰值: 2GPU/芯片 x 256 位 x 2500 MHz内存时钟 x 2 DDR/8位/字节 = 320 GB/s
指令比:字节 4.58 TFLOPS/320 GB/s =13.6 个指令: 1个字节

2.3

这里我们回顾了之前thread_index的计算方式,比如说,对于二维矩阵,理论上:

ix=threadIdx.x+blockIdx.x×blockDim.x
iy=threadIdx.y+blockIdx.y×blockDim.y
matrix[ix][iy] // 当前矩阵的位置

当然,由于我们一般使用一位矩阵来模拟,所以这里要接着上面做一次映射:

int idx=ix+iy*ny;
matrix[idx] // 当前矩阵

事实上,对于二维甚至三维矩阵,一般情况下优秀的工程实践是使用float *matrix来模拟二维/三维,而不是使用float ***matrix
多次指针dereference,导致非连续内存访问,影响性能。
cudaMalloc也不支持float ***matrix格式的内存申请

事实上,我们一般使用cudaMallocPitch()cudaMalloc3D() 来优化2D和3D内存的分配,对于内存对齐有好处。
配合cudaMemcpy2D()cudaMemcpy3D()一起使用。

感谢gpt老师给出了一个例子,这里的row代表了rowHead,指的是每行首的指针地址,要学好c才能看懂哦:->

__global__ void printThreadIndex(float *A, int pitch, int nx, int ny) {
    int ix = threadIdx.x + blockIdx.x * blockDim.x;
    int iy = threadIdx.y + blockIdx.y * blockDim.y;
    
    if (ix < nx && iy < ny) {
        // 使用 pitch 计算二维索引
        float *row = (float *)((char *)A + iy * pitch);  // 计算行地址
        float value = row[ix];  // 读取列值
    }
}

谭升这里举了一个例子很有趣:

__global__ void sumMatrix(float * MatA,float * MatB,float * MatC,int nx,int ny)
{
    int ix=threadIdx.x+blockDim.x*blockIdx.x;
    int iy=threadIdx.y+blockDim.y*blockIdx.y;
    int idx=ix+iy*ny;
    if (ix<nx && iy<ny)
    {
      MatC[idx]=MatA[idx]+MatB[idx];
    }
}

这个矩阵相加函数依赖idx作为全局所有thread的总id,在一维矩阵上进行操作,每个线程只操作一次

所以,
<<<(128,128),(32,32)>>>
<<<(524288,1),(32,1)>>>
<<<(128,4096),(32,1)>>>
都能够运行成功,只是由于内存物理排版导致的速度不同

2.4

nvidia-smi,在不会cuda的时候就应该知道如何去查看显卡配置了。

有各种API来支持你查找GPU的各种参数,此处不在多说,用到什么查什么即可。

posted @ 2025-03-11 22:16  真昼小天使daisuki  阅读(114)  评论(0)    收藏  举报