8 全局变量_大规模并行处理器编程实战8

news/2024/7/7 21:06:05 标签: 8 全局变量

第八章 并行模式:卷积 ----- 介绍常数存储器和高速缓存

8-10 章 介绍重要的并行计算模式,是很多并行算法的基础。本章是介绍卷积,以及从存储器的优化思路去优化卷积代码。

1. 卷积:

1.1 卷积并行特点:

a. 每个输出元素的计算都是相互独立的,可并行。

b. 输入元素之间具有相当程度的共享,比如核参数。

挖坑1:幽灵元素,卷积的边界缺失元素;对分块算法的复杂度和效率影响很大。

1.2 初级的卷积核函数(一维卷积):

__global__ void convolution_1D_basic_kernel(float* N, float* M, float* P, int Mask_Width, int Width){
    // 参数,输入数组N,掩码数组M,结果数组P,掩码大小,输入数组大小
    
    // 计算输入元素索引,一位卷积,网格设计为一维大小
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    // 中间加权和变量放在 寄存器 中缓存,而不是全局变量的P中,访问速度快。
    float Pvalue = 0;

    // 卷积计算开始位置
    int N_start_point = i - (Mask_Width / 2);

    // 开始计算一次加权和
    for(int j = 0; j < Mask_Width; j++){
        // 边界判断,核函数内控制流产生分支,性能影响
        if(N_start_point + j >= 0 && N_start_point + j < Width){
            Pvalue += N[N_start_point + j] * M[j];  // **************
        }
    }
    P[i] = Pvalue;
}

问题:1. 由于幽灵元素产生if 控制流的多样性,会影响性能。但是如果大数组,小掩码,影响有限。2. 存储器带宽,可以计算 CGMA值(浮点运算和全局存储器访问的比值) = (加法和乘法两次浮点运算)/(对N 和 M的两次数据访问) = 1,非常低,下文先主要解决这个问题。

2. 常数存储器和高速缓存

观察核参数的特点:a. 尺寸小(比如常用3*3 7*7等);b. 核参数计算时数值不变;c. 所有线程都要访问,而且是以相同的顺序访问。

由上特性,优化方案为,通过将核参数M放入 常数存储器 ,利用高速缓存。先介绍常数存储器和高速缓存的特点。

2.1 常数存储器:

5bdbe06a700f907f8608988e40551819.png

2.1.1 常数存储器特点:

与全局存储器相同都为DRAM (挖坑2);对所有线程块可见;

但在核函数执行期间,值不能被修改;

2.1.2 常数存储器容量查询方法:

cudaDeviceProp prop;  // 结构体
if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
    printf("totalConstMem : %d.n", prop.totalConstMem);
}

2.1.3 常数存储器变量声明:

往往声明为全局变量,与C中的全局变量性质相同。主机代码中:

#define MAX_MASK_WIDTH 10
__constant__ float M[MAX_MASK_WIDTH]; // 常量存储器变量 声明关键字为 __constant__ 
cudaMemcpyToSymbol(M, h_M, Mask_Width * sizeof(float));    // 此copy函数告诉CUDA,在核函数执行期间,变量值是不能改变

2.1.4 优化后的代码:

__global__ void convolution_1D_basic_kernel(float* N, float* P, int Mask_Width, int Width){
    // 参数,输入数组N,结果数组P,掩码大小,输入数组大小
    // 注: M 采用全局变量声明,不需要参数传递
    
    // 计算输入元素索引,一位卷积,网格设计为一维大小
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    // 中间加权和变量放在 寄存器 中缓存,而不是全局变量的P中,访问速度快。
    float Pvalue = 0;

    // 卷积计算开始位置
    int N_start_point = i - (Mask_Width / 2);

    // 开始计算一次加权和
    for(int j = 0; j < Mask_Width; j++){
        // 边界判断,核函数内控制流产生分支,性能影响
        if(N_start_point + j >= 0 && N_start_point + j < Width){
            Pvalue += N[N_start_point + j] * M[j];  // **************
        }
    }
    P[i] = Pvalue;
}

2.1.5 解决挖坑2:

解决挖坑2,常量存储器和全局都为DRAM,为什么常量存储器中,访问速度快?

对于常数存储器中的变量,CUDA认为在核函数执行时是不变的,所以会把他们放到高速缓存中。

2.2 高速缓存:

870c2ba8b15d99f455a305a2e723a957.png

2.2.1 高速缓存的引入逻辑:

全局存储器访问一个变量,动辄数百甚至数千时钟周期,通常比算术操作慢得多,导致存储墙问题(DRAM 的长延迟和有限带宽成为几乎所有现代处理器的性能瓶颈)。为解决引入高速缓存(多级),减少访问DRAM次数。

2.2.2 高速缓存与共享存储器:

共享存储器是显式的,透明的。通过__shared__关键字声明,并显式的将全局存储器变量复制到共享存储器变量中。

高速缓存,程序只需要简单访问原始变量,不能程序显式的指定。处理器硬件自动保留一些最近或者经常使用的变量到高速缓存中,并记住他们的原始DRAM地址。当保留的变量再次被使用时,硬件会从他们的地址中判断出高速缓存中已经保留他们的副本,即可提供变量值,从而消除对DRAM的访问需求。(高速缓存还需深入)

2.2.3 多级高速缓存

速度和尺寸上的权衡。

L1 高速缓存,直接连接到处理器核心,延迟和带宽与处理器接近。通常只连接一个处理器核(挖坑3)。尺寸通常只有16KB~64KB。

L2 高速缓存,有几十个时钟周期的访问延迟。通常被多个处理器核或者CUDA设备中的多核流处理器SM共享。尺寸,128K~1M。

L3 高速缓存,几MB。

2.2.4 缓存一致性,解决挖坑3:

L1 只连接一个处理器核,导致在修改数据时,出现缓存一致性问题。缓存一致性机制保证其他处理器缓存中的数据及时更新。CPU通常支持处理器核心之间的缓存一致性。

大规模并行处理器中,实现缓存一致性是困难的,开销很大。虽然提供两级高速缓存,但是为了最大化利用硬件资源,提高算术运算吞吐率,通常不提供缓存一致性机制。

2.2.5 高速缓存和常数存储器

好在,常数存储器不修改值,不存在缓存一致性问题。硬件直接将他们放在L1高速缓存中。

缓存设计中,优化了大量线程的广播,一个warp 中访问同一个常数存储器变量时,高速缓存能为需要数据提供巨大的带宽。(ummmmmmmmm)

2.3 优化结果:

回到2.1.4 优化的代码,通过高速缓存访问M中元素,可简单假定对掩码数组的访问不会增加DRAM带宽,CGMA 提升到了2

3. 分块一维卷积

上文是从掩码参数M入手优化的,现在对输入变量N优化代码。我们引入第五章的数据分块算法,将全局存储器中的数据复制到共享存储器中,供本线程块使用,减少DRAM的访问,提高CGMA值。

3.1 第一种分块策略

0eba2cc3771b33612698271b053047b6.png

分块策略如上图。一个线程块中,将此线程块所需的所有数据都加载到共享存储器中。数据分块,由网格大小、线程块大小划分。但因为卷积算法,需要前后两个输入数组元素的值。例如分块1 第一个线程N[4]位置计算时需要N[2] N[3] 的值,所以需要把他们也加载到本线程块的共享内存。因为共享内存只在本线程块可见,所以上图N[2],N[3]既被分块0 加载到共享内存中,也会被加载到分块1的共享内存中,这类称为光环元素或边缘元素。其他的称为中间元素。

另外,如分块0左侧存在幽灵元素,分块3右侧存在幽灵元素,所以分块0 和分块3 称为 边界块。分块2 和 分块1 为 中间块 。优化代码如下:

__global__ void convolution_1D_basic_kernel(float* N, float* P, int Mask_Width, int Width){
    // 参数,输入数组N,结果数组P,掩码大小,输入数组大小
    // 注: M 采用全局变量声明,不需要参数传递
    
    // 计算输入元素索引,一位卷积,网格设计为一维大小
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 共享内存声明,大小为线程块大小 + 左右光环元素大小
    __shared__ float N_ds[TILE_SIZE + MAX_MASK_WIDTH - 1];

    // 左侧光环元素载入,  计算 光环元素 对应index, 取上一个线程块的元素
    int halo_index_left = (blockIdx.x - 1)*blockDim.x + threadIdx.x;
    // 光环元素,如线程块0,1,2 中的[2][3][6][7][10][11]这几个位置,进行填充
    if(threadIdx.x >= blockDim.x - n){
        // 把他们填充到对应共享内存数组的开头几个数中,边界填充为0
        N_ds[threadIdx.x - (blockDim.x - n)] = 
                        (halo_index_left < 0) ? 0 : N[halo_index_left];
    }

    // 填充中间元素, 根据线程id 找到输入数组相应位置填充
    N_ds[n + threadIdx.x] = N[blockIdx.x * blockDim.x + threadIdx.x];

    // 填充右侧光环元素,与填充左侧思路一致。先找到下一个线程块光环元素对应输入数组的id,
    // 将他们填充到相应线程块的共享内存中
    int halo_index_right = (blockIdx.x + 1)*blockDim.x + threadIdx.x;
    // 光环元素,如线程块0,1,2 中的[2][3][6][7][10][11]这几个位置,进行填充
    if(threadIdx.x < n){
        // 把他们填充到对应共享内存数组的开头几个数中,边界填充为0
        N_ds[threadIdx.x + blockDim.x + n] = 
                        (halo_index_right >= Width) ? 0 : N[halo_index_right];
    }

    // 重要,栅栏同步,保证数据先复制到共享内存,才能开始下一步计算
    __syncthreads();

    // 中间加权和变量放在 寄存器 中缓存,而不是全局变量的P中,访问速度快。
    float Pvalue = 0;

    // 开始计算一次加权和
    for(int j = 0; j < Mask_Width; j++){
        // 从共享内存中 和 常量内存中取数,计算卷积
        Pvalue += N_ds[threadIdx.x + j] * M[j];
    }
    P[i] = Pvalue;
}

3.2 优化分析:

优化前,对数组N的访问次数为:每个线程,需要访问掩模大小的量去做加权求和。所以每个线程块就需要访问blockDim.x * Mask_Width次或者为blockDim.x * (2n + 1)。

注:对于边界块,因为存在幽灵元素,会减少访存操作次数,减少量为 n(n + 1) / 2。对于大线程块、小掩码数组来说,幽灵元素产生的影响是巨大的(猜测应该是减少量和需要访问量的比值)。

优化后,每个线程块访问数组N,加载到共享内存。加载次数为线程块大小 + 左右两侧光环元素,即 blockDim.x + 2n。

优化前与优化后访问N的比值为:

blockDim.x * (2n + 1) / blockDim.x + 2n

假设 blockDim.x = 128 n = 5时,比值为10.13;blockDim.x = 32 n = 5时,比值为8.14。可以得到结论,在使用较小线程块和分块时,访存减少的的比率可能低于预期。不过较小的分块尺寸会经常用在片上存储器容量不足的情况下,比如计算二维三维卷积时。

进一步的估算,一般 blockDim.x 会比 n 大的多,消除小项 n, 可以估计比值为,(2n + 1) 也就是掩模大小,也就是整体访存减少比率为掩码数组的大小

3.3 另一种分块一维卷积

此优化思路的依据是,GPU提供 L1 L2 高速缓存。L1 为每个SM私有的,L2 为所有SM共享的。存在一个事实是:光环元素可能会存在L2 缓存中。比如 线程块1 使用光环元素时,因为之前线程块0 访问而存储到L2上了,这样线程块1 访问光环元素时,直接访问L2 而不是 DRAM了。所以本优化思路是只将中间元素存储到共享内存中,光环元素直接去访问原始输入数组了。

__global__ void convolution_1D_basic_kernel(float* N, float* P, int Mask_Width, int Width){
    // 参数,输入数组N,结果数组P,掩码大小,输入数组大小
    // 注: M 采用全局变量声明,不需要参数传递
    
    // 计算输入元素索引,一位卷积,网格设计为一维大小
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 共享内存声明,大小只是线程块大小了,  相比上节,无左右光环元素大小
    __shared__ float N_ds[TILE_SIZE];

    // 只需要填充中间元素即可
    N_ds[threadIdx.x] = N[i];

    // 重要,栅栏同步,保证数据先复制到共享内存,才能开始下一步计算
    __syncthreads();

    // 本线程中, 共享内存中取数相对N的 开始和结尾idx,用于判断 数是从共享取,还是DRAM
    int This_tile_start_point = blockIdx.x * blockDim.x;
    int Next_tile_start_point = (blockIdx.x + 1) * blockDim.x;
    // 卷积计算的开始 idx,是相对于N 中的idx
    int N_start_point = i - (Mask_Width / 2);

    // 中间加权和变量放在 寄存器 中缓存,而不是全局变量的P中,访问速度快。
    float Pvalue = 0;

    // 开始计算一次加权和
    for(int j = 0; j < Mask_Width; j++){
        // 本轮卷积加权和计算的 取数idx 相对于N输入数组,
        int N_index = N_start_point + j;
        // 保证不是幽灵元素
        if(N_index >= 0 && N_index < Width){
            // 如果说在共享内存中,从共享内存中取
            if(N_index >= This_tile_start_point && N_index < Next_tile_start_point){
                Pvalue += N_ds[threadIdx.x + j - (Mask_Width / 2)] * M[j];
            } else{
                // 如果不在共享内存中,从N中取
                Pvalue += N[N_index] * M[j];
            }
        }
    }
    P[i] = Pvalue;
}

http://www.niftyadmin.cn/n/1536042.html

相关文章

一个测试基础面试题——如何测试web银行开户

之前面试被问到过这样一个问题&#xff0c;自己答的都是一些UI界面上的case&#xff0c;看了一些大神的关于这类面试题的总结才知道自己差的不是一点半点&#xff0c;今天也总结下。 内管银行开户&#xff0c;有账号、用户名、用户证件类型、证件号三个栏位&#xff0c;针对这个…

jsessionid和jwt_jsessionid

ntellij idea插件2020-12-25 17:03:27第一次从Eclipse转到idea时&#xff0c;那时候确实挺难受的&#xff0c;各种不是习惯&#xff0c;各种抵触&#xff0c;后来设置一些eclipse的使用习惯&#xff0c;发现越来越好用了&#xff0c;作为一名开发&#xff0c;工欲善其事必先利其…

软件外包行业分析

目前已经进驻上海的知名全球服务外包企业包括埃森哲、优利、NCS、博朗、爱特优科等 目录&#xff1a;[0] - 为什么要对大学生谈软件外包&#xff1f;[1] - 什么是软件外包&#xff1f;[2] - 软件为什么要外包&#xff1f;[3] - 为什么要承接软件外包[4] - 做软件外包有前途吗&…

php检测目标服务器是否宕机_记录一次服务器宕机分析过程(1)-排查问题

发现宕机&启动coredump最近给版署审核版本用的服务器每隔几天就会宕机&#xff0c;情况比较类似&#xff0c;都是大厅服的进程直接没有掉了。查看服务器log&#xff0c;没有发现什么线索。怀疑是服务器进程崩溃掉了&#xff0c;于是开启了服务器的coredump&#xff0c;等待…

关于sizeof()的一些思考

原始连接&#xff1a;http://lostinmymind.blogchina.com/index.html关键字&#xff1a;sizeof&#xff0c;字节对齐&#xff0c;多继承&#xff0c;虚拟继承&#xff0c;成员函数指针 前向声明&#xff1a; sizeof&#xff0c;一个其貌不扬的家伙&#xff0c;引无数菜鸟竟折…

Hibernate入门----配置文件

一、Hibernate简介 hibernate是一个开源的&#xff0c;轻量级的&#xff0c;持久成ORM框架。 Hibernate是一个开放源代码的对象关系映射框架&#xff0c;它对JDBC进行了非常轻量级的对象封装&#xff0c;它将POJO与数据库表建立映射关系&#xff0c;是一个全自动的 orm框架&…

python的json格式输出_python中json格式数据输出实现方式

python中json格式数据输出实现方式主要使用json模块&#xff0c;直接导入import json即可。小例子如下&#xff1a;#codingUTF-8import jsoninfo{}info["code"]1info["id"]1900info["name"]张三info["sex"]男list[info,info,info]data…

我的架构经验小结(一)-- 常用的架构模型

经过这几年的积累&#xff0c;在系统架构方面逐渐积累了一些自己的经验&#xff0c;到今天有必要对这些经验作个小结。在我的架构思维中&#xff0c;主要可以归类为三种架构模型&#xff1a;3/N层架构、“框架&#xff0b;插件”架构、地域分布式架构。 一&#xff0e;三种架构…