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

从本章起,将关注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.mfbz.cn/a/887731.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

系统架构设计师-下午案例题(2021年下半年)

1.试题一(共25分) 阅读以下关于软件架构设计与评估的叙述在答题纸上回答问题1和问题2。 【说明】某公司拟开发一套机器学习应用开发平台支持用户使用浏览器在线进行基于机器学习的智能应用开发活动。该平台的核心应用场景是用户通过拖拽算法组件灵活定义机器学习流程&#xf…

【含开题报告+文档+PPT+源码】基于SSM + Vue的养老院管理系统【包运行成功】

开题报告 随着社会的发展和经济的进步&#xff0c;人口老龄化问题逐渐凸显。统计数据显示&#xff0c;全球范围内的老龄人口比例正在逐年上升&#xff0c;养老需求也随之增长。养老院作为提供专业养老服务的机构&#xff0c;承担着照料老人、提供医疗保健和社交活动等责任。传…

什么是pip? -- Python 包管理工具

前言 不同的编程语言通常都有自己的包管理工具&#xff0c;这些工具旨在简化项目的依赖管理、构建过程和开发效率&#xff0c;同时促进代码的复用和共享。每个包管理工具都有其独特的特点和优势&#xff0c;开发者可以根据自己的编程语言和项目需求选择合适的包管理工具。 pip是…

车辆重识别(2021ICML改进的去噪扩散概率模型)论文阅读2024/9/29

所谓改进的去噪扩散概率模型主要改进在哪些方面&#xff1a; ①对数似然值的改进 通过对噪声的那个方差和T进行调参&#xff0c;来实现改进。 ②学习 这个参数也就是后验概率的方差。通过数据分析&#xff0c;发现在T非常大的情况下对样本质量几乎没有影响&#xff0c;也就是说…

TIM的PWM模式

定时器的工作流程: 定时器对时钟传来的脉冲次数计数&#xff0c;并且在次数到达范围值时触发中断。如向下计数模式时为0&#xff0c;向上计数为达到自动重装载计时器的值时触发中断。 四个输出比较单元 更改占空比的函数 STM32里面的定时器有多个定时器。 如TIM1、TIM2…

k8s 之安装metrics-server

作者&#xff1a;程序那点事儿 日期&#xff1a;2024/01/29 18:25 metrics-server可帮助我们查看pod的cpu和内存占用情况 kubectl top po nginx-deploy-56696fbb5-mzsgg # 报错&#xff0c;需要Metrics API 下载 Metrics 解决 wget https://github.com/kubernetes-sigs/metri…

nginx 负载均衡1

遇到的问题 大型网站都要面对庞大的用户量&#xff0c;高并发&#xff0c;海量数据等挑战。为了提升系统整体的性能&#xff0c;可以采用垂直扩展和水平扩展两种方式。 垂直扩展&#xff1a;在网站发展早期&#xff0c;可以从单机的角度通过增加硬件处理能力&#xff0c;比如 C…

LeetCode讲解篇之239. 滑动窗口最大值

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们维护一个长度为k的窗口&#xff0c;然后窗口从数组最左边一直移动到最右边&#xff0c;记录过程中窗口中的最大值&#xff0c;就是答案 我们每次查询长度为k的窗口最大值是什么时间复杂度是O(k)的&#xff0…

黑神话:仙童,数据库自动反射魔法棒

黑神话&#xff1a;仙童&#xff0c;数据库自动反射魔法棒 Golang 通用代码生成器仙童发布了最新版本电音仙女尝鲜版十一及其介绍视频&#xff0c;视频请见&#xff1a;https://www.bilibili.com/video/BV1ET4wecEBk/ 此视频介绍了使用最新版的仙童代码生成器&#xff0c;将 …

使用 Python 遍历文件夹

要解决这个问题&#xff0c;使用 Python 的标准库可以很好地完成。我们要做的是遍历目录树&#xff0c;找到所有的 text 文件&#xff0c;读取内容&#xff0c;处理空行和空格&#xff0c;并将处理后的内容合并到一个新的文件中。 整体思路&#xff1a; 遍历子目录&#xff1…

计算机毕业设计 基于Hadoop的智慧校园数据共享平台的设计与实现 Python 数据分析 可视化大屏 附源码 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

国外电商系统开发-运维系统拓扑布局

点击列表中设备字段&#xff0c;然后定位到【拓扑布局】中&#xff0c;可以看到拓扑发生了变化 再回头&#xff0c;您再次添加一个服务器到系统中&#xff0c;并且选择该服务器的连接节点为您刚才创建的“SDN路由器”&#xff0c;保存后&#xff0c;您可以看到这个服务器连接着…

RabbbitMQ篇(环境搭建 - 下载 安装)(持续更新迭代)

目录 一、Windows 1. 下载安装程序 2. 安装配置erlang 3. 安装rabbitMQ 4. 验证 二、Linux 1. 下载rpm包 1.1. 下载Erlang的rpm包 1.2. 下载socat的rpm包 1.3. 下载RabbitMQ的rpm包 2. 安装 2.1. 安装Erlang 2.2. 安装socat 2.3. 安装RabbitMQ 3. 启动RabbitMQ服…

小程序原生-利用setData()对不同类型的数据进行增删改

1. 声明和绑定数据 wxml文件 <view> {{school}} </view> <view>{{obj.name}}</view> <view id"{{id}}" > 绑定属性值 </view> <checkbox checked"{{isChecked}}"/> <!--算数运算--> <view>{{ id …

数理统计(第1章第2节:一些常用的抽样分布)

目录 统计量的概率分布称为“抽样分布” 1. 正态母体的子样平均数的抽样分布 正态分布 2. 卡方分布 3. t分布 4. F分布 5. 例题 6. 总结 统计量的概率分布称为“抽样分布” 1. 正态母体的子样平均数的抽样分布 正态分布 若随机变量X的概率密度为&#xff1a; 则称X服…

Qt开发技巧(九)去掉切换按钮,直接传样式文件,字体设置,QImage超强,巧用Qt的全局对象,信号槽断连,低量数据就用sqlite

继续讲一些Qt开发中的技巧操作&#xff1a; 1.去掉切换按钮 QTabWidget选项卡有个自动生成按钮切换选项卡的机制&#xff0c;有时候不想看到这个烦人的切换按钮&#xff0c;可以设置usesScrollButtons为假&#xff0c;其实QTabWidget的usesScrollButtons属性最终是应用到QTabWi…

重学SpringBoot3-集成Redis(三)之注解缓存策略设置

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;三&#xff09;之注解缓存策略设置 1. 引入 Redis 依赖2. 配置 RedisCacheManager 及自定义过期策略2.1 示例代码&#xff1a;自定…

Vue - 路由用法

前端路由就是URL中的hash与组件之间的对应关系。Vue Router是Vue的官方路由。 组成&#xff1a; VueRouter&#xff1a;路由器类&#xff0c;根据路由请求在路由视图中动态渲染选中的组件。<router-link>&#xff1a;请求链接组件&#xff0c;浏览器会解析成<a>。…

【易上手快捷开发新框架技术】nicegui组件button用法庖丁解牛深度解读源代码IDE运行和调试通过截图为证

传奇开心果微博文系列 前言一、button 组件基本用法1. 最基本用法示例2. 创建带图标按钮 二、button按钮组件样式定制1. 按钮的尺寸调整2. 改变颜色示例3. 按钮的自定义字体大小4. 圆角形状示例5. 自定义边框6. 添加阴影7. 复合按钮8. 浮动按钮9. 可扩展浮动操作按钮QFAB10. 按…

【MAUI】CommunityToolkit社区工具包介绍

一、为什么需要声明式开发 .NET的MVVM,始于WPF,很古典,它甚至可能是现代前端框架“声明式开发”的鼻祖。声明式开发,之所以出现,是因为命令式开发在UI层和代码层上无法解耦的问题。如下图所示: 1、命令式开发:后台代码需要调用UI层的控件(label.Text),如果更新UI层…