《CUDA编程》5.获得GPU加速的关键

news2024/11/28 8:26:01

从本章起,将关注CDUA程序的性能,即执行速度

1 用CUDA事件计时

在前几章中,使用的是C++的<time.h>库进行程序运行计时,CUDA也提供了一种基于CUDA event的计时方式,用来给一段CUDA代码进行计时,这里只介绍基于cudaEvent_t的计时方式,下面是一代码框架:

#include <cuda_runtime.h>
#include "error_check.cuh"

cudaEvent_t start, stop;

CHECK(cudaEventCreate(&start));
CHECK(cudaEventCreate(&stop));
CHECK(cudaEventRecord(start));
cudaEventQuery(start);//不能使用CHECK,因为可能返回cudaErrorNotReady,但并不是代码报错

/*需要计时的代码块*/

CHECK(cudaEventRecord(stop));
CHECK(cudaEventSynchronize(stop));
float time;
CHECK(cudaEventElapsedTime(&time, start, stop));
printf("time: %f ms\n", time);

CHECK(cudaEventDestroy(start));
CHECK(cudaEventDestroy(stop));

  1. 首先定义两个cudaEvent_t的变量startstop,并用cudaEventCreate()初始化。
  2. 在需要计时的代码块运行之前,把start传入cudaEventRecord()
  3. 若是在TCC驱动模式的GPU中,cudaEventQuery(start);可以省略;若是处于WDDM驱动模式中,则必须保留。关于这两种模式,后面会讨论。
  4. 在需要计时的代码块运行之后,把stop传入cudaEventRecord()
  5. 使用cudaEventSynchronize(stop)让主机等待事件stop被记录完毕
  6. 调用 cudaEventElapsedTime() 函数计算 startstop 这两个事件之间的时间差(单位是 ms)并输出到屏幕。
  7. 调用 cudaEventDestroy() 函数销毁 startstop 这两个 CUDA 事件。

1.1 举例说明

下面是一段利用cudaEvent_t进行计时的代码,在调用核函数add前后进行计时,注意,为了能够进行单精度和双精度的比较,需要定义一个宏变量,以便在编译时选择精度

#include <cuda.h>
#include <cuda_runtime.h>
#include <math.h>
#include <stdio.h>
#include "error_check.cuh"
#ifdef USE_DP
    typedef double real;
    const real EPSILON = 1.0e-15;
#else
    typedef float real; 
    const real EPSILON = 1.0e-6f;
#endif 


const real EPS = 1.0e-15;
const real a = 1.23;
const real b = 2.34;
const real c = 3.57;

// 希望 add 函数在 GPU 上执行
__global__ void add(const real* x, const real* y, real* z);
void check(const real* z, const int N);

int main(void) {
    const int N = 100000000; // 定义数组的长度为 10 的 8 次方
    const int M = sizeof(real) * N; // 每个数组所需的字节数

    // 分配host内存
    real* h_x = (real*)malloc(M);
    real* h_y = (real*)malloc(M);
    real* h_z = (real*)malloc(M);


    for (int n = 0; n < N; ++n) {
        h_x[n] = a;
        h_y[n] = b;
    }

    //分配device内存
    real* d_x, * d_y, * d_z;
    CHECK(cudaMalloc((void**)&d_x, M));
    CHECK(cudaMalloc((void**)&d_y, M));
    CHECK(cudaMalloc((void**)&d_z, M));

    // 将数据从主机复制到设备上
    CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice));
    


    const int block_size = 128;
    // 计算网格尺寸,确保所有元素都能被处理
    const int grid_size = (N + block_size - 1) / block_size;

    cudaEvent_t start, stop;
    CHECK(cudaEventCreate(&start));
    CHECK(cudaEventCreate(&stop));
    CHECK(cudaEventRecord(start));


    // 调用内核函数在设备中进行计算
    add << <grid_size, block_size >> > (d_x, d_y, d_z);

    CHECK(cudaEventRecord(stop));
    CHECK(cudaEventSynchronize(stop));

    // 将计算结果从设备复制回主机
    CHECK(cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost));
    check(h_z, N);
    float elapsed_time;
    CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
    printf("Elapsed time: %f ms\n", elapsed_time);

    // 释放内存
    CHECK(cudaEventDestroy(start));
    CHECK(cudaEventDestroy(stop));
    free(h_x);
    free(h_y);
    free(h_z);
    CHECK(cudaFree(d_x));
    CHECK(cudaFree(d_y));
    CHECK(cudaFree(d_z));
    return 0;
}

__global__ void add(const real* x, const real* y, real* z) {
    const int n = blockIdx.x * blockDim.x + threadIdx.x;
    z[n] = x[n] + y[n];
}

void check(const real* z, const int N) {
    bool has_error = false;
    for (int n = 0; n < N; ++n) {
        if (fabs(z[n] - c) > EPS) {
            has_error = true;
        }
    }
    printf("Has error: %d\n", has_error);
}


①单精度编译过程和输出结果

nvcc -o singel_cuda addFunction.cu -arch=sm_75

运行后输出如下:
在这里插入图片描述
②双精度编译过程和输出结果

nvcc -DUSE_DP -o double_cuda addFunction.cu -arch=sm_75

运行后输出如下:
在这里插入图片描述

观察结果,我们发现单精度的运行时间是10.624ms;双精度的运行时间是21.490ms

1.2 该计算任务并不适合使用GPU进行加速

我们把1.1代码中的数据复制步骤也加入到计时当中,观察耗时情况:
①单精度输出结果
在这里插入图片描述
①双精度输出结果
在这里插入图片描述
观察发现,核函数的运行时间连整体运行时间的10%都没有,若是算上CPU和GPU之间的传输时间,把该程序放入GPU中运算的性能,可能还不如直接在CPU上运行。

这里可以使用CUDA自带的nvprof工具对程序进行性能分析:

nvprof .\singel_cuda.exe

输出结果如下:
在这里插入图片描述
根据分析结果可以得出

  • Host-to-Device (HtoD) 内存复制:
    占总 GPU 时间的 58.98%,耗时 127.74 毫秒。
  • Device-to-Host (DtoH) 内存复制:
    占总 GPU 时间的 36.13%,耗时 78.259 毫秒。
  • CUDA 内核函数 add 的执行:
    占总 GPU 时间的 4.89%,耗时 10.590 毫秒

这意味着在总的 GPU 活动时间中,大约 95.11% 的时间都花在了内存复制上,而只有 4.89% 的时间用于实际的计算。所以这样的任务,其实是不适合使用GPU进行 “加速” 的,那么什么任务才能真正的发挥GPU加速能力呢?

2 影响GPU加速的关键因素

2.1 数据传输比例

从上一个例子我们可以得出,如果一任务仅仅是计算两个数组的和,那么用GPU可能比用CPU还慢,因为花在CPU与GPU之间传输的时间比计算时间还要多太多。

所以一个适合使用GPU加速的任务,一定是数据传输占比时间少的任务,尽量让一些操作在GPU中完成,避免过多数据经过PCIe传输,例如做10000次数组相加,只在开头和结尾进行数据传输(H to D/D to H)

下面是把上面代码的add操作重复1000次:

    // 调用内核函数在设备中进行计算 1000 次
    for (int i = 0; i < 1000; ++i) {
        add << <grid_size, block_size >> > (d_x, d_y, d_z);
    }

运行性能分析,结果输出如下:
在这里插入图片描述
性能分析结果显示,add函数运行耗时占比97.96%,数据传输占比是2.04%,所以这样的任务就适合使用GPU加速

2.2 算术强度(arithmetic intensity)

算术强度: 计算过程中浮点运算次数与读写内存字节数的比例。

在上述例子中,我们只进行了加法运算,接下来我们修改核函数,进行一些更复杂的数学运算,代码如下:

__global__ void add(const real* x, const real* y, real* z) {
    const int n = blockIdx.x * blockDim.x + threadIdx.x;
    real x_val = x[n];
    real y_val = y[n];

    // 复杂的数学运算
    real result = sin(x_val) + cos(y_val) + exp(x_val * y_val) / (1.0 + x_val * y_val);
    result += log10(x_val + 1.0) * sqrt(y_val);

    // 最终结果
    z[n] = result;
}

性能分析结果如下:
在这里插入图片描述
性能分析结果显示,运算时间占比为31.71%,传输数据时间占比为68.29%,比之前只做一次加法运算的操作更适合用GPU加速(之前运算时间占比是4.89%)

2.3 并行规模

并行规模: 指的是在GPU上同时执行的线程数量。

因为GPU上的线程是可以并行执行的,在设计核函数时,尽量让设备中的所有线程都要参与到计算之中,可以最大程度的加速CUDA程序,下面是两幅图:
在这里插入图片描述
N是指放到GPU上进行运算的数据规模。

  1. 左图N在3次方和4次方时,耗时差距不大,是因为这两个时候,GPU的线程还有空闲,即所有数据都有线程在处理。从5次方开始,因为数据规模以及超过线程数量了,所以需要排队等待计算,故而随着N的增加,耗时成比例增加
  2. 有图是和CPU相比,GPU的运算加速比。可以发现在3次方和4次方时,加速比增大,因为GPU有大量的线程可以并行,远远比CPU运算快。在达到5次方时,线程以及利用完毕,也得排队等待计算,所以加速比几乎不变

3 总结

在编写CUDA程序时,一定要做到以下三点:

  1. 减少主机和设备之间的数据传输时间占比、也要减少数据传输次数
  2. 提高核函数的算术强度
  3. 增大核函数的并行规模

附:下面给出CUDA自带的数学函数库网站

http://docs.nvidia.com/cuda/cuda-math-api
包含幂函数、三角函数、指数函数、对数函数等,在编写代码时,要注意单精度和双精度的使用范围,例如有的计算精度不高的计算可以使用单精度,可以大大提升CUDA程序性能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2192400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字化营销:开启商业新征程

在当今数字化时代&#xff0c;数字化营销已成为企业竞争的关键利器。 数字化营销有着完整的体系。首先是目标设定与市场分析&#xff0c;明确如提高品牌知名度、增加销量等目标&#xff0c;并通过市场调研了解消费者需求。接着是客户洞察与定位&#xff0c;收集客户数据构建画像…

安装最新 MySQL 8.0 数据库(教学用)

安装 MySQL 8.0 数据库&#xff08;教学用&#xff09; 文章目录 安装 MySQL 8.0 数据库&#xff08;教学用&#xff09;前言MySQL历史一、第一步二、下载三、安装四、使用五、语法总结 前言 根据 DB-Engines 网站的数据库流行度排名&#xff08;2024年&#xff09;&#xff0…

面试官:MySQL 什么时候会出现死锁问题?为什么不推荐使用RR隔离级别?

欢迎关注公众号 【11来了】 &#xff0c;持续 MyBatis 源码系列内容&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址&#xff1a;点击查看文章导读&#xff01; 感谢你的关注&#xff…

WindowsTerminal 美化-壁纸随机更换

目录 一. 相关网址二. 壁纸随机更换思路三. 指定 WindowsTermina 壁纸路径四. 编写脚本&#xff0c;随机替换壁纸4.1 powershell脚本4.2 .bat批处理脚本 四. 配置定时任务&#xff0c;添加触发器五. 效果 一. 相关网址 官方下载 Windows Terminal 官方Github微软商店 美化 Oh …

链式二叉树及二叉树各种接口的实现(C)

二叉树的性质 若规定根节点的层数为1&#xff0c;则一棵非空二叉树的第 i i i层上最多有 2 i − 1 2^{i-1} 2i−1个结点.若规定根节点的层数为1&#xff0c;则深度为h的二叉树的最大结点数是 2 h − 1 2^{h}-1 2h−1对任何一棵二叉树&#xff0c;如果度为0其叶结点个数为 n 0 …

Semantic Communication Meets Edge Intelligence——构造终端共享的知识图谱指导无线物联网通信中文本的传输

论文链接&#xff1a; IEEE Xplore Full-Text PDF:https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber9979702 1. 背景 随着自动驾驶、智能城市等应用的发展&#xff0c;移动数据流量将大幅增加。传统的香农信息论&#xff08;CIT&#xff09;通信系统已接近其带…

内网穿透工具ngrok

写作背景 最近在公司内购淘了个MAC电脑&#xff0c;想当个Linux服务器起Docker搭建环境用&#xff0c;现在问题是如何在公网上能访问到MAC这个机器上的资源。 之前写了一篇文章Mac当作云服务器&#xff0c;你真的会搞吗 最近想重启一下这台老伙计了&#xff0c;发现ngrok还是…

CIKM 2024 | 时空数据(Spatial-temporal)论文总结

CIKM 2024于10月21号-10月25号在美国爱达荷州博伊西举行&#xff08;Boise, Idaho, USA&#xff09; 本文总结了CIKM 2024有关时空数据&#xff08;spatial-temporal data&#xff09;的相关论文&#xff0c;主要包含交通预测&#xff0c;插补&#xff0c;事故预测&#xff0c…

计算机网络——http和web

无状态服务器——不维护客户端 怎么变成有状态连接 所以此时本地建立代理—— 若本地缓存了——但是服务器变了——怎么办&#xff1f;

CSS元素堆叠

通常我们可能会认为 HTML 网页是个二维的平面&#xff0c;因为页面中的文本、图像或者其它元素都是按照一定顺序排列在页面上的&#xff0c;每个元素之间都有一定的间隙&#xff0c;不会重叠。然而&#xff0c;实际的网页其实是三维的&#xff0c;元素之间可能会发生堆叠&#…

《python语言程序设计》2018版第8章19题几何Rectangle2D类(中)-同志们我要起飞了

前言 昨天的原始绘制两个矩形的代码段draw_rec2原始draw_rec2运行结果我们不是上面往右转90.我怎么往左转90不对吗??? ☺️结果利用已建立完的Rectangle2D类来实现Rectangle2D类的代码可以找上集看,今天是锻炼的一天好几个倒立体式解锁了.祝大家愉快 经过昨天晚上的努力我终…

Python画笔案例-078 绘制 颜色渐变之coloradd

1、绘制纯 颜色渐变之coloradd 通过 python 的turtle 库绘制 颜色渐变之coloradd,如下图: 2、实现代码 绘制 颜色渐变之coloradd,以下为实现代码: """颜色渐变之coloradd.py本程序需要coloradd模块支持,请在cmd窗口,即命令提示符下输入pip install colorad…

VMware桥接模式无法连接网络

windows下打开控制面板&#xff0c;找到WLAN&#xff0c;记住下面的名称&#xff08;带有VMware的都是虚拟机的网卡&#xff0c;要找到物理主机的网卡&#xff09; 回到VMware&#xff0c;编辑——打开虚拟网络编辑器 桥接选择上面的WLAN下的网络名称&#xff0c;确定即可。&…

tortorise数据库迁移变化aerich

数据库迁移 使用场景&#xff0c;当需要修改定义的数据库中表的数据时&#xff0c;就可以利用aerich进行迁移改动 例如 class Asset(models.Model):aid fields.CharField(max_length50, pkTrue)asset_name fields.CharField(max_length150)target_name fields.CharField(…

GO网络编程(三):海量用户通信系统1:登录功能初步

一、准备工作 需求分析 1)用户注册 2)用户登录 3)显示在线用户列表 4)群聊(广播) 5)点对点聊天 6)离线留言 主界面 首先&#xff0c;在项目根目录下初始化mod&#xff0c;然后按照如下结构设计目录&#xff1a; 海量用户通信系统/ ├── go.mod ├── client/ │ ├──…

血液细胞计数与检测(BCCD)数据集教程

BCCD 数据集&#xff1a;血液细胞检测与计数-CSDN博客文章浏览阅读431次&#xff0c;点赞5次&#xff0c;收藏3次。BCCD 数据集&#xff1a;血液细胞检测与计数 BCCD_Dataset BCCD (Blood Cell Count and Detection) Dataset is a small-scale dataset for blood cells detecti…

U盘格式化别担心,数据恢复神器来了!

一、恢复数据的紧迫性和希望 别担心&#xff0c;小编我有幸深陷U盘数据丢失的境地&#xff0c;因此通过不懈的努力与反复试验&#xff0c;今日在此为广大读者分享一次趟雷后恢复数据工具的真实体验&#xff1b;当U盘格式化后&#xff0c;你可能会面临数据的丢失&#xff0c;但…

超简单 Flux 换背景工作流:结合最新 ControlNet 深度 Depth 模型

在本篇文章中&#xff0c;我们将深入探讨如何使用 Flux ControlNet Depth 模型进行换背景。这种方法是我之前基于 Flux 模型换背景工作流的简化版。虽然旧的工作流程功能强大&#xff0c;但它非常复杂且运行缓慢。今天&#xff0c;我们将学习一个更快速、更易用的替代方案。 F…

变电站红外检测数据集 1180张 变电站红外 标注voc yolo 13类

变电站红外检测数据集 1180张 变电站红外 标注voc yolo 13类 变电站红外检测数据集 名称 变电站红外检测数据集 (Substation Infrared Detection Dataset) 规模 图像数量&#xff1a;1185张图像。类别&#xff1a;13种设备类型。标注个数&#xff1a;2813个标注。 数据划分…

【全球顶级域名后缀】

数据时间: 2024.10.6 广告: 五分钟申请SSL证书 (手机电脑都能用) ["aaa","aarp","abarth","abb","abbott","abbvie","abc","able","abogado","abudhabi","ac"…