cuda编程学习——CUDA全局内存性能优化(八)

news2024/11/16 0:53:20

前言

参考资料:

高升博客
《CUDA C编程权威指南》
以及 CUDA官方文档
CUDA编程:基础与实践 樊哲勇

文章所有代码可在我的GitHub获得,后续会慢慢更新

文章、讲解视频同步更新公众《AI知识物语》,B站:出门吃三碗饭

在各种设备内存中,全局内存具有最低的访问速度(最高的延迟), 往往是一个CUDA程序性能的瓶颈,所以值得特别地关注。

1:全局内存的合并和非合并访问

关于全局内存的访问模式,有合并(coalesced)与非合并(uncoalesced)之分。合并 访问指的是一个线程束对全局内存的一次访问请求(读或者写)导致最少数量的数据传输,否则称访问是非合并的。

探讨合并度与全局内存访 问模式之间的关系

为简单起见,我们主要以全局内存的读取和仅使用 L2 缓存的情况为例进行下述讨论。 在此情况下,一次数据传输指的就是将 32 字节的数据从全局内存(DRAM)通过 32 字节的 L2 缓存片段(cache sector)传输到SM。
考虑一个线程束访问单精度浮点数类型的全局 内存变量的情形。因为一个单精度浮点数占有 4 个字节,故该线程束将请求 128 字节的数 据。在理想情况下(即合并度为 100% 的情况),这将仅触发 128/32 = 4 次用 L2 缓存的数据传输。那么,在什么情况下会导致多于 4 次数据传输呢?

为了回答这个问题,我们首先需要了解数据传输对数据地址的要求:在一次数据传输 中,从全局内存转移到L2缓存的一片内存的首地址一定是一个最小粒度(这里是32 字节)的整数倍
例如,一次数据传输只能从全局内存读取地址为 0-31 字节、32-63 字节、64-95字节、96-127 字节等片段的数据。

再接着思考,那么如何保证一次数据传输中内存片段的首地址为最小粒度的整数倍呢? 或者问:如何控制所使用的全局内存的地址呢?答案是:使用CUDA运行时 API 函数(如我们常用的 cudaMalloc)分配的内存的首地址至少是 256 字节的整数倍。

1:顺序的合并访问。我们考察如下的核函数和相应的调用:

void __global__ add(float *x, float *y, float *z) {
int n = threadIdx.x + blockIdx.x * blockDim.x; z[n] = x[n] + y[n];
}
add<<<128, 32>>>(x, y, z);

其中,x、y 和 z 是由 cudaMalloc() 分配全局内存的指针。很容易看出,核函数中对 这几个指针所指内存区域的访问都是合并的。例如,第一个线程块中的线程束将访问 数组x中第 0-31 个元素,对应128字节的连续内存,而且首地址一定是256字节的整数倍。这样的访问只需要 4 次数据传输即可完成,所以是合并访问,合并度为 100%。

案例

本节将通过一个矩阵转置的例子讨论全局内存的合理使用。矩阵转置是线性代数中一 个基本的操作。我们这里仅考虑行数与列数相等的矩阵,即方阵。学完本节后,读者可以思考如何在CUDA中对非方阵进行转置

假设一个矩阵A的矩阵元为Aij,则其转置矩阵B = AT 的矩阵元为
在这里插入图片描述

2.1矩阵的复制

__global__ void copy(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * TILE_DIM + threadIdx.x;
    const int ny = blockIdx.y * TILE_DIM + threadIdx.y;
    const int index = ny * N + nx;
    if (nx < N && ny < N)
    {
        B[index] = A[index];
    }
}
    const int grid_size_x = (N + TILE_DIM - 1) / TILE_DIM;  
    const int grid_size_y = grid_size_x;  
    const dim3 block_size(TILE_DIM, TILE_DIM);  
    const dim3 grid_size(grid_size_x, grid_size_y);
    copy<<<grid_size, block_size>>>(d_A, d_B, N);

注释:
(1)在调用核函数copy时,我们用了二维的网格和线程块。 在该问题中,并不是一定要使用二维的网格和线程块,因为矩阵中的数据排列本质上依然是一维的。然而,在后面的矩阵转置问题中,使用二维的网格和线程块更为方便。
(2)程序中的 TILE_DIM 是一个整型常量,取值为 32,指的是一片(tile)矩阵的维度 (dimension,即行数)。我们将一片一片地处理一个大矩阵。其中的一片是一个 32 × 32 的矩阵。每一个二维的线程块将处理一片矩阵。
(3)和线程块一致,网格也用二维的,维度为待处理矩阵的维度 N 除以线程块维度。
例如,假如N为128,则grid_size_x和grid_size_y都 是 128/32 = 4。也就是说,核函数所用网格维度为 4 × 4,线程块维度为 32 × 32。此时在 核函数 copy 中的 gridDim.x 和 gridDim.y 都等于 4,而 blockDim.x 和 blockDim.y 都等于 32。读者应该注意到,一个线程块中总的线程数目为 1024,刚好为所允许的最大值。

2.1矩阵的转置

if (nx < N && ny < N) B[ny * N + nx] = A[ny * N + nx];
从数学的角度来看,这相当于做了Bij = Aij 的操作。
如果要实现矩阵转置,即Bij = Aji 的 操作,可以将上述代码换成
if (nx < N && ny < N) B[nx * N + ny] = A[ny * N + nx];
or
if (nx < N && ny < N) B[ny * N + nx] = A[nx * N + ny];
注意看其区别变化

以上两条语句都能实现矩阵转置,但是它们将带来不同的性能。
与它们对应的核函数分 别为 transpose1 和 transpose2,代码如下

__global__ void transpose1(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * blockDim.x + threadIdx.x;
    const int ny = blockIdx.y * blockDim.y + threadIdx.y;
    if (nx < N && ny < N)
    {
        B[nx * N + ny] = A[ny * N + nx];
    }
}

__global__ void transpose2(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * blockDim.x + threadIdx.x;
    const int ny = blockIdx.y * blockDim.y + threadIdx.y;
    if (nx < N && ny < N)
    {
        B[ny * N + nx] = A[nx * N + ny];
    }
}

可以看出,在核函 数transpose1中,对矩阵A中数据的访问(读取)是顺序的,但对矩阵B中数据的访问(写 入)不是顺序的

在核函数transpose2中,对矩阵A中数据的访问(读取)不是顺序的,但 对矩阵B中数据的访问(写入)是顺序的

在不考虑数据是否对齐的情况下,我们可以说核 函数transpose1对矩阵A和B的访问分别是合并的和非合并的,而核函数transpose2对矩阵 A 和 B 的访问分别是非合并的和合并的。

性能结果:

核 函数 transpose1 的执行时间为 5.3 ms,而核函数 transpose2 的执行时间为 2.8 ms。
以上 两个核函数中都有一个合并访问和一个非合并访问,但为什么性能差别那么大呢?
这是因为在核函数transpose2中,读取操作虽然是非合并的,但利用了第 6 章提到的只读数据 缓存的加载函数 __ldg()。从帕斯卡架构开始,如果编译器能够判断一个全局内存变量在 整个核函数的范围都只可读(如这里的矩阵 A),则会自动用函数 __ldg() 读取全局内存, 从而对数据的读取进行缓存,缓解非合并访问带来的影响。对于全局内存的写入,则没有 类似的函数可用。这就是以上两个核函数性能差别的根源。
改进:
所以,在不能同时满足读取和写入都是合并的情况下,一般来说应当尽量做到合并的写入


#include <stdio.h>
#include <math.h>
#include <stdio.h>
#include<stdint.h>
#include<cuda.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>

#define CHECK(call)                                   \
do                                                    \
{                                                     \
    const cudaError_t error_code = call;              \
    if (error_code != cudaSuccess)                    \
    {                                                 \
        printf("CUDA Error:\n");                      \
        printf("    File:       %s\n", __FILE__);     \
        printf("    Line:       %d\n", __LINE__);     \
        printf("    Error code: %d\n", error_code);   \
        printf("    Error text: %s\n",                \
            cudaGetErrorString(error_code));          \
        exit(1);                                      \
    }                                                 \
} while (0)

#ifdef USE_DP
typedef double real;
#else
typedef float real;
#endif

const int NUM_REPEATS = 10;
const int TILE_DIM = 32;

void timing(const real* d_A, real* d_B, const int N, const int task);
__global__ void copy(const real* A, real* B, const int N);
__global__ void transpose1(const real* A, real* B, const int N);
__global__ void transpose2(const real* A, real* B, const int N);
__global__ void transpose3(const real* A, real* B, const int N);
void print_matrix(const int N, const real* A);

int main(void)
{
    /*if (argc != 2)
    {
        printf("usage: %s N\n", argv[0]);
        exit(1);
    }*/
    const int N = 100;

    const int N2 = N * N;
    const int M = sizeof(real) * N2;
    real* h_A = (real*)malloc(M);
    real* h_B = (real*)malloc(M);
    for (int n = 0; n < N2; ++n)
    {
        h_A[n] = n;
    }
    real* d_A, * d_B;
    CHECK(cudaMalloc(&d_A, M));
    CHECK(cudaMalloc(&d_B, M));
    CHECK(cudaMemcpy(d_A, h_A, M, cudaMemcpyHostToDevice));

    printf("\ncopy:\n");
    timing(d_A, d_B, N, 0);
    printf("\ntranspose with coalesced read:\n");
    timing(d_A, d_B, N, 1);
    printf("\ntranspose with coalesced write:\n");
    timing(d_A, d_B, N, 2);
    printf("\ntranspose with coalesced write and __ldg read:\n");
    timing(d_A, d_B, N, 3);

    CHECK(cudaMemcpy(h_B, d_B, M, cudaMemcpyDeviceToHost));
    if (N <= 10)
    {
        printf("A =\n");
        print_matrix(N, h_A);
        printf("\nB =\n");
        print_matrix(N, h_B);
    }

    free(h_A);
    free(h_B);
    CHECK(cudaFree(d_A));
    CHECK(cudaFree(d_B));
    return 0;
}

void timing(const real* d_A, real* d_B, const int N, const int task)
{
    const int grid_size_x = (N + TILE_DIM - 1) / TILE_DIM;
    const int grid_size_y = grid_size_x;
    const dim3 block_size(TILE_DIM, TILE_DIM);
    const dim3 grid_size(grid_size_x, grid_size_y);

    float t_sum = 0;
    float t2_sum = 0;
    for (int repeat = 0; repeat <= NUM_REPEATS; ++repeat)
    {
        cudaEvent_t start, stop;
        CHECK(cudaEventCreate(&start));
        CHECK(cudaEventCreate(&stop));
        CHECK(cudaEventRecord(start));
        cudaEventQuery(start);

        switch (task)
        {
        case 0:
            copy << <grid_size, block_size >> > (d_A, d_B, N);
            break;
        case 1:
            transpose1 << <grid_size, block_size >> > (d_A, d_B, N);
            break;
        case 2:
            transpose2 << <grid_size, block_size >> > (d_A, d_B, N);
            break;
        case 3:
            transpose3 << <grid_size, block_size >> > (d_A, d_B, N);
            break;
        default:
            printf("Error: wrong task\n");
            exit(1);
            break;
        }

        CHECK(cudaEventRecord(stop));
        CHECK(cudaEventSynchronize(stop));
        float elapsed_time;
        CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
        printf("Time = %g ms.\n", elapsed_time);

        if (repeat > 0)
        {
            t_sum += elapsed_time;
            t2_sum += elapsed_time * elapsed_time;
        }

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

    const float t_ave = t_sum / NUM_REPEATS;
    const float t_err = sqrt(t2_sum / NUM_REPEATS - t_ave * t_ave);
    printf("Time = %g +- %g ms.\n", t_ave, t_err);
}

__global__ void copy(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * TILE_DIM + threadIdx.x;
    const int ny = blockIdx.y * TILE_DIM + threadIdx.y;
    const int index = ny * N + nx;
    if (nx < N && ny < N)
    {
        B[index] = A[index];
    }
}

__global__ void transpose1(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * blockDim.x + threadIdx.x;
    const int ny = blockIdx.y * blockDim.y + threadIdx.y;
    if (nx < N && ny < N)
    {
        B[nx * N + ny] = A[ny * N + nx];
    }
}

__global__ void transpose2(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * blockDim.x + threadIdx.x;
    const int ny = blockIdx.y * blockDim.y + threadIdx.y;
    if (nx < N && ny < N)
    {
        B[ny * N + nx] = A[nx * N + ny];
    }
}

__global__ void transpose3(const real* A, real* B, const int N)
{
    const int nx = blockIdx.x * blockDim.x + threadIdx.x;
    const int ny = blockIdx.y * blockDim.y + threadIdx.y;
    if (nx < N && ny < N)
    {
        B[ny * N + nx] = __ldg(&A[nx * N + ny]);
    }
}

void print_matrix(const int N, const real* A)
{
    for (int ny = 0; ny < N; ny++)
    {
        for (int nx = 0; nx < N; nx++)
        {
            printf("%g\t", A[ny * N + nx]);
        }
        printf("\n");
    }
}

在这里插入图片描述

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

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

相关文章

Python3数据分析与挖掘建模(8)检验

1. 假设检验 1.1 概述 假设检验是一种统计推断方法&#xff0c;用于对一个或多个总体参数提出关于其取值的假设&#xff0c;并根据样本数据对这些假设进行检验。假设检验的目的是根据样本数据提供统计上的证据&#xff0c;以便对总体参数的假设进行接受或拒绝。 在假设检验中…

JAVA基础 - 如何使用ClassLoader?

1. CLASSLOADER是什么 ClassLoader&#xff0c;类加载器。用于将CLASS文件动态加载到JVM中去&#xff0c;是所有类加载器的基类(Bootstrap ClassLoader不继承自ClassLoader)&#xff0c;所有继承自抽象的ClassLoader的加载器&#xff0c;都会优先判断是否被父类加载器加载过&a…

C++数据结构:二叉树之二(二叉搜索树)

文章目录 前言一、二叉搜索树的概念二、代码详解1、构建节点2、构建二叉树类3、插入方法4、删除方法5、四种遍历方法6、测试代码 总结 前言 前文已经讲了二叉树概念&#xff0c;并搞出一个数组存储的没写具体实用意义的二叉树&#xff0c;这篇文章将讲解二叉树的另一种存储方式…

限量内测名额释放:微信云开发管理工具新功能

我们一直收到大家关于云数据库管理、快速搭建内部工具等诉求&#xff0c;为了给大家提供更好的开发体验&#xff0c;结合大家的诉求&#xff0c;云开发团队现推出新功能「管理工具」&#xff0c;现已启动内测&#xff0c;诚邀各位开发者参与内测体验。 什么是「管理工具」 管…

当节点内存管理遇上 Kubernetes:自动调度与控制

原理 在现代的容器化环境中&#xff0c;节点资源的管理是一个重要的任务。特别是对于内存资源的管理&#xff0c;它直接影响着容器应用的性能和可用性。在 Kubernetes 中&#xff0c;我们可以利用自动调度和控制的机制来实现对节点内存的有效管理。本文将介绍一种基于 Bash 脚…

EM中等效原理

EM中等效原理 一、基本简介 电磁等效定理对于简化许多问题的解是有用的。此外&#xff0c;它们还提供了对麦克斯韦系统电磁场行为的物理见解。它们与唯一性定理和惠更斯原理密切相关。一个应用是它们在研究来自孔径天线或来自激光腔输出的辐射中的用途。 等效源原理&#xf…

3.2 掌握RDD算子

一、准备工作 &#xff08;一&#xff09;准备文件 1、准备本地系统文件 2、把文件上传到HDFS &#xff08;二&#xff09;启动Spark Shell 1、启动HDFS服务 2、启动Spark服务 3、启动Spark Shell 二、掌握转换算子 &#xff08;一&#xff09;映射算子 - map() …

Sketch在线版免费使用,Windows也能用的Sketch!

Sketch 的最大缺点是它对 Windows/PC 用户不友好。它是一款 Mac 工具&#xff0c;无法在浏览器中运行。此外&#xff0c;使用 Sketch 需要安装其他插件才能获得更多响应式设计工具。然而&#xff0c;现在有了 Sketch 网页版工具即时设计替代即时设计&#xff01; 即时设计几乎…

通达信凹口平量柱选股公式,倍量柱之后调整再上升

凹口平量柱是一组量柱形态&#xff0c;表现为量柱两边高、中间扁平或圆底的形态。如下图所示&#xff0c;左右各有一根高度持平的高量柱&#xff0c;中间夹杂着三五根甚至更多根低量柱。 凹口平量柱选股公式需要结合量柱以及K线&#xff0c;主要考虑以下三点&#xff1a; 1、倍…

git各阶段代码修改回退撤销操作

git push origin master 的含义是将本地当前分支的提交推送到名为 origin 的远程仓库的 master 分支上。 各阶段代码修改回退撤销的操作 case1 git checkout -- . 修改了文件内容但没还有git add 或git commit时撤销当前目录下所有文件的修改 case2 当完成了git add 之后&a…

项目管理:面对未知的挑战时,如何获取和使用信息?

一项实验展示了人们在面对未知的挑战时&#xff0c;对信息的获取和使用的影响。在下面的实验中&#xff0c;三组人被要求步行到十公里外的三个村庄。 第一组人没有任何信息&#xff0c;只跟着向导走。他们在走了短短的两三公里后就开始抱怨和情绪低落&#xff0c;同时感到疲惫…

2022年天府杯全国大学生数学建模竞赛E题地铁线路的运营与规划解题全过程文档及程序

2022年天府杯全国大学生数学建模竞赛 E题 地铁线路的运营与规划 原题再现&#xff1a; 地铁是一种非常绿色快捷的交通出行方式&#xff0c;全国各大城市也都在如火如荼地进行地铁线路建设与规划。但乘坐地铁有时候会感觉特别拥挤&#xff0c;这一时期我们称为高峰期。如何合理…

sqlserver中的merge into语句

merge into语句是用来合并两张表的数据的&#xff0c;比如我们想把一张表的数据批量更新到另外一张表&#xff0c;就可以用merge into语句。具体有哪些业务场景呢&#xff1f; 1.数据同步 2.数据转换 3.基于源表对目标表进行增&#xff0c;删&#xff0c;改的操作。 实践步骤…

JavaScript了解调用带参函数,无参函数的代码

以下为JavaScript了解调用带参函数&#xff0c;无参函数的程序代码和运行截图 目录 前言 一、带参函数 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 二、无参函数 2.1 运行流程及思想 2.2 代码段 2.3 JavaScript语句代码 2.4 运行截图 前言…

让代码创造童话,共建快乐世界:六一儿童节特辑

让代码创造童话&#xff0c;共建快乐世界&#xff1a;六一儿童节特辑 六一儿童节即将来临&#xff0c;这是一个属于孩子们的快乐节日。为了让这个节日更加有趣&#xff0c;我们发起了“让代码创造童话&#xff0c;共建快乐世界”六一活动。在这个活动中&#xff0c;我们邀请您…

使用Tensorrt对YOLOv5目标检测的代码进行加速

文章目录 1. 前言2. 官网3. 安装依赖3.1. 安装OpenCV3.1.1. 安装3.1.2. 添加环境变量3.1.3. 查看版本 3.2. 安装TensorRT3.2.1. 下载3.2.2. 安装3.2.3. 添加环境变量 4. 下载项目5. 生成WTS模型6. cmake6.1. 生成Makefile6.1.1. 配置CMakeLists.txt6.1.1.1. 修改编译依赖的路径…

通过python采集1688商品评论数据封装接口、1688评论数据接口

1688商品评论数据是指在1688网站上对商品的评价和评论信息。这些信息包括买家对商品的使用、品质、包装、服务等方面的评价和意见&#xff0c;可以帮助其他用户更好地了解商品的优缺点和性能&#xff0c;从而做出更明智的购买决策。 1688网站是中国最大的B2B电子商务网站之一&…

RK3566调试EC20

参考博客&#xff1a;RK3568开发笔记-buildroot移远EC20模块调试记录 一、内核配置 cd 到kernel目录&#xff0c;执行make ARCHarm64 menuconfig&#xff0c; Device Drivers >USB support > USB Serial Converter support 选中 USB driver for GSM and CDMA modems选…

04.hadoop上课笔记之java编程和hbase

1.win查看服务 netstat -an #linux也有#R数学建模语言 SCALAR 2.java连接注意事项,代码要设置用户 System.setProperty("HADOOP_USER_NAME", "hadoop");3.伪分布式的好处(不用管分布式细节,直接连接一台机器…,适合用于学习) 4.官方文档 查看类(static |…

5个UI设计师必备的Figma汉化插件

即时设计插件广场提供了许多有用的 UI 插件&#xff0c;帮助优化产品设计过程。其中&#xff0c;产品组件库 Figma 汉化插件对常用的 PC 端和移动端组件进行了筛选&#xff0c;使其更加聚焦和精简。PC 端组件包括基础、按钮、菜单和其他元素&#xff0c;移动端组件包括基础、按…