《CUDA编程》7.全局内存的合理使用

news2024/10/25 15:22:58

上一章简单的介绍了一下各种内存,本章开始详细讲解各个内存的合理使用,在所有设备中,全局内存的访问速度最慢,是CUDA程序的一个性能瓶颈,所以值得特别关注

1 全局内存的合并与非合并访问

对全局内存的访问将触发内存事务(memory transaction),也就是数据传输(data transfer)。

从费米架构开始,在启用了L1缓存的情况下,对全局内存的读取将首先尝试经过 L1 缓存;如果未中,则接着尝试经过L2缓存;如果再次未中,则直接从DRAM读取。一次数据传输处理的数据量在默认情况下是 32 字节

关于全局内存的访问模式,有合并(coalesced)与非合并(uncoalesced)之分。

1.1 合并访问

多个线程(一个线程束)在同一时间内访问全局内存中的连续地址,从而减少内存访问的次数,提高内存带宽的利用率。合并访问是优化CUDA程序性能的重要手段之一。

1.1.1 基本概念

  1. 线程束: CUDA中的线程是以线程束的形式执行的。一个线程束包含32个线程,这些线程在同一个SM上并行执行。
    如果线程束中的所有线程访问的是连续的内存地址,那么这些访问可以被合并成一次或几次较大的内存事务,从而提高内存访问的效率。

  2. 合并访问的条件:
    ①连续性: 线程束中的所有线程访问的地址必须是连续的,例如,如果一个warp包含32个线程,每个线程访问一个4字节的float数据,那么这32个线程应该访问连续的128字节
    ②对齐: 访访问的起始地址应对4字节对齐(对于float类型),理想情况下应对128字节对齐,以减少内存事务的数量。

1.1.2 举例

观察下面数组相加的核函数:

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

①连续性: 在这个例子中,每个线程的索引 n 是通过 threadIdx.x + blockIdx.x * blockDim.x 计算的,所以第一个线程束中的线程0访问 x[0] 和 y[0],线程1访问 x[1] 和 y[1],依此类推,直到线程31访问 x[31] 和 y[31],符合连续性

②对齐: float 类型的数据占用4字节,因此只要数组 x 和 y 的起始地址是对4字节对齐的,每个线程访问的地址也会是对4字节对齐的,理想情况下,起始地址应对128字节对齐。而数组x和y是由cudaMalloc分配的内存,所以首地址至少是 256 字节的整数倍,所以符合对齐

综上所述,是合并访问。

例如:一个线程束中的32个线程将分别访问数组x中0~31个float元素,后者是连续的128字节内存,所以最快只需要4次传输就能完成,由于线程是连续地址、访问的地址也对齐了,所以只需要4次就能完成传输,合并度是4/4=100%

1.2 非合并访问

1.2.1 不对齐的非合并访问

在线程索引后面+1

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

①连续性: int n = threadIdx.x + blockIdx.x * blockDim.x + 1;多了一个偏移量,所以线程0将访问 x[1] 和 y[1],线程1将访问 x[2] 和 y[2],依此类推,不符合连续性
②对齐: 由于线程0访问 x[1],而不是 x[0],这意味着访问的起始地址不是128字节对齐的,而是128的倍数+4,所以不符合对齐

综上所述,是非合并访问。

例如:一个线程束中的32个线程将分别访问数组x中1~32个float元素,后者是连续的128字节内存,所以最快只需要4次传输就能完成,但是由于线程索引多了一个偏移量,则线程访问的起始地址不再与128字节对齐

①理想情况: 如果线程从数组索引0开始,每个线程访问连续的数组元素(例如,线程0访问x[0],线程1访问x[1],线程2访问x[2],…),那么32个线程的访问可以被合并为 1次传输操作。
②实际情况: 线程访问的内存范围是x[1]到x[32],但因为访问地址不是对齐的,所以被分割为x[1]到x[31]和x[32]

前者虽然传输了128/432=4次数据,但是x[0]的数据是没用的,所以还需要后者再传输1次,所以总共传输了5次。

合并度=4/5=80%

1.2.2 跨越时的非合并访问

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

因为这里 的每一对数据都不在一个连续的 32 字节的内存片段,故该线程束的访问将触发 32 次 数据传输,合并度为 4/32 = 12:5%。

2 例子:矩阵转置

将通过一个矩阵转置的例子讨论全局内存的合理使用,假设一个矩阵 A i j A_{ij} Aij ,则其转置矩阵 B j i = A T B_{ji}=A^T Bji=AT ,取
在这里插入图片描述
则其转置矩阵为
在这里插入图片描述

2.1 矩阵复制

在讨论矩阵转置之前,先讨论一个更简单的问题,矩阵复制,代码如下:

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

#define TILE_DIM 32  // 定义每个block的线程块维度

// 将矩阵 A 复制到矩阵 B
__global__ void cpy_matrix(const float* A, float* B, const int N) {
    // 计算当前线程的全局索引(行列坐标)
    const int nx = blockIdx.x * TILE_DIM + threadIdx.x;  // 计算当前线程的列索引
    const int ny = blockIdx.y * TILE_DIM + threadIdx.y;  // 计算当前线程的行索引

    // 计算当前线程在矩阵中的线性索引(行优先存储)
    const int idx = ny * N + nx;  // 线性索引公式:行索引 * 行长度 + 列索引

    // 检查当前线程是否在矩阵范围内,避免越界访问
    if (nx < N && ny < N) {
        // 将矩阵 A 中对应位置的值复制到矩阵 B 中
        B[idx] = A[idx];
    }
}

int main() {
    // 定义矩阵大小
    const int N = 1024; 
    const int size = N * N * sizeof(float);

    // 主机上分配内存
    float* h_A = (float*)malloc(size);
    float* h_B = (float*)malloc(size);

    // 初始化矩阵数据
    for (int i = 0; i < N*N; i++) {
        h_A[i] = 1.0f;
    }

    // 在设备上分配内存
    float* d_A, *d_B;
    CHECK(cudaMalloc((void**)&d_A, size));
    CHECK(cudaMalloc((void**)&d_B, size));

    // 将主机矩阵数据拷贝到设备
    CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice));

    // 创建线程块和线程块网格
    dim3 threads(TILE_DIM, TILE_DIM);// 线程块是32 * 32 
    dim3 gridSize((N + TILE_DIM - 1) / TILE_DIM, (N + TILE_DIM - 1) / TILE_DIM);// 确保矩阵大小能被线程块整除
        
    // 调用核函数,将矩阵 A 复制到矩阵 B
    cpy_matrix<<<gridSize, threads>>>(d_A, d_B, N);

    // 将矩阵B 从设备拷贝到主机
    CHECK(cudaMemcpy(h_B, d_B, size, cudaMemcpyDeviceToHost));

    // 检查复制结果,确保正确性
    bool success = true;
    for (int i = 0; i < N * N; i++) {
        if (h_B[i] != h_A[i]) {
            success = false;
            break;
        }
    }

    if (success) {
        std::cout << "Matrix copy successful!" << std::endl;
    }
    else {
        std::cout << "Matrix copy failed!" << std::endl;
    }

    // 释放主机和设备内存
    free(h_A);
    free(h_B);
    cudaFree(d_A);
    cudaFree(d_B);

    return 0;
}

之前我们的线程索引都是一维的,为什么这里要使用二维线程索引呢?主要与处理二维数据(如矩阵)更自然和高效

①矩阵本身是二维结构:
矩阵是由行和列组成的二维数据结构,因此使用二维的线程索引来直接表示矩阵的行和列,使得每个线程可以自然地与矩阵中的一个元素相对应,在核函数中:

  • nx 代表线程的列索引(横坐标)。
  • ny 代表线程的行索引(纵坐标)。

每个线程根据其nx和ny,访问矩阵中对应位置的元素,从而完成并行化的矩阵操作。

②简化索引计算:
使用二维线程索引可以简化矩阵元素的索引计算,在二维线程索引中:

  • 行索引 ny = blockIdx.y * TILE_DIM + threadIdx.y:block的y索引加上线程的y索引,可以直接对应矩阵中的行。
  • 列索引 nx = blockIdx.x * TILE_DIM + threadIdx.x:block的x索引加上线程的x索引,可以直接对应矩阵中的列。

2.2 使用全局内存进行矩阵转置

2.2.1代码实现

在上述代码的核函数中:

const int idx = ny * N + nx;
if (nx < N && ny < N) {
	B[idx] = A[idx];
}

可以简化为以下代码:

if (nx < N && ny < N) {
	B[ny * N + nx] = A[ny * N + nx];
}

意味着把矩阵通过行优先存储转化为一维数组,然后复制给矩阵 B B B,所以我们只需要略微修改代码,就能完成转置的效果,看似方法类似,但其实性能不同:
法①:

if (nx < N && ny < N) {
	// 行索引转列索引,实现矩阵转置
	B[nx * N + ny] = A[ny * N + nx]; 
}

在这里插入图片描述

法②

if (nx < N && ny < N) {
	// 列索引转行索引,实现矩阵转置
	B[ny * N + nx] = A[nx * N + ny]; 
}

在这里插入图片描述

2.2.2 结果分析

为什么方法类似,性能差距这么大呢?

我们将行索引转列索引命名为htol,列索引转行索引命名为ltoh,可以看出:
①在htol中: 对矩阵 A 中数据的读取是顺序的,即对A的访问是合并访问;对矩阵 B 中数据的写入不是顺序的,即对B的访问是非合并访问

②在ltoh中: 对矩阵A中的数据读取不是顺序的,即对A的访问是非合并访问;对矩阵B中的数据写入是顺序的,即对B的访问是合并访问。

这是因为,在核函数ltoh中,读取操作虽然是非合并的,但利用了只读数据缓存的加载函数 __ldg()

从帕斯卡架构开始,如果编译器能够判断一个全局内存变量在 整个核函数的范围都只可读(如这里的矩阵 A),则会自动用函数 __ldg() 读取全局内存, 从而对数据的读取进行缓存,缓解非合并访问带来的影响。

2.3 总结

所以,在全局内存下,如果不能同时满足读取和写入都是合并的情况下,一般来说应当尽量做到写入操作是合并访问。

注意: 在使用开普勒架构和麦克斯韦架构的 GPU 时,需要明显地使用 __ldg() 函数。

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

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

相关文章

LabVIEW如何实现高精度定时器

在LabVIEW中实现高精度定时器通常需要考虑以下几个方面&#xff1a;定时器的精度要求、操作系统的调度机制、硬件资源&#xff08;如计时器、触发器&#xff09;等。以下是几种常见的实现方式&#xff1a; ​ 1. 使用 Wait(ms) 或 Wait Until Next ms Multiple VI 这两个函数…

【无人机设计与控制】PID_积分滑模_积分反步四旋翼无人机轨迹跟踪控制算法

摘要 本文基于四旋翼无人机设计与控制&#xff0c;提出了一种结合PID控制、积分滑模控制以及积分反步控制的轨迹跟踪算法。该算法通过调节无人机的运动轨迹&#xff0c;提升其在复杂环境下的稳定性与抗扰动能力。实验结果表明&#xff0c;该算法能有效改善无人机的轨迹跟踪精度…

【python实操】python小程序之计算对象个数、游戏更新分数

引言 python小程序之计算对象个数、游戏更新分数 文章目录 引言一、计算对象个数1.1 题目1.2 代码1.3 代码解释1.3.1 代码结构1.3.2 模块解释1.3.3 解释输出 二、游戏更新分数2.1 题目2.2 代码2.3 代码解释2.3.1 定义 Game 类2.3.2 创建 Game 实例并调用方法 三、思考3.1 计算对…

C++之String类模拟实现(下)

片头 哈喽~小伙伴们&#xff0c;在上一篇中&#xff0c;我们讲解了C的string类的相关函数&#xff0c;这一章中&#xff0c;我们将继续深入学习string类函数&#xff0c;准备好了吗&#xff1f;咱们开始咯~ 五、对内容进行修改 ⑤insert函数 在指定位置插入字符或者字符串 …

基于Raspberry Pi人脸识别自动门

人脸识别自动门 简介 在当今数字化时代&#xff0c;智能家居安全变得越来越重要。今天&#xff0c;我要向大家介绍一个结合了安全性与便利性的项目——人脸识别自动门。这个项目通过在门上实施基于面部识别的高级安全系统&#xff0c;使用摄像头验证房主的面部&#xff0c;自…

重学SpringBoot3-集成Spring Boot Actuator

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Spring Boot Actuator 1. 什么是 Spring Boot Actuator&#xff1f;2. Spring Boot Actuator 的核心功能3. Spring Boot 3 中集成 Actuator3.1 添加…

ElasticSearch是什么?

1.概述 Elasticsearch 是一个基于 Apache Lucene 构建的开源分布式搜索引擎和分析引擎。它专为云计算环境设计&#xff0c;提供了一个分布式的、高可用的实时分析和搜索平台。Elasticsearch 可以处理大量数据&#xff0c;并且具备横向扩展能力&#xff0c;能够通过增加更多的硬…

2014年国赛高教杯数学建模C题生猪养殖场的经营管理解题全过程文档及程序

2014年国赛高教杯数学建模 C题 生猪养殖场的经营管理 某养猪场最多能养10000头猪&#xff0c;该养猪场利用自己的种猪进行繁育。养猪的一般过程是&#xff1a;母猪配种后怀孕约114天产下乳猪&#xff0c;经过哺乳期后乳猪成为小猪。小猪的一部分将被选为种猪&#xff08;其中公…

20240727 影石 笔试

文章目录 1、选择题1.11.21.31.41.51.61.71.81.91.10 2、简答题2.12.22.32.42.52.62.72.8 3、编程题3.1 岗位&#xff1a;云台嵌入式工程师-2025校招 题型&#xff1a;10 道选择题&#xff0c;8 道简答题&#xff0c;1 道编程题 1、选择题 1.1 【多选】以下关于DMA的描述哪些…

Pytest中fixture含返回值时如何隐式自动应用?

在我们使用 Pytest 框架进行自动化测试时&#xff0c;强大的 fixture 夹具为框架的灵活应用提供了极大的便利。比如我们可以利用 fixture 的autouse属性&#xff0c;使它在测试方法的不同范围层级上自动生效。但如果要引用fixture的返回&#xff0c;我们通常还是要明确指定&…

FMEA 在智能制造中的应用与挑战_SunFMEA

【大家好&#xff0c;我是唐Sun&#xff0c;唐Sun的唐&#xff0c;唐Sun的Sun。一站式数智工厂解决方案服务商】 FMEA&#xff08;失效模式与影响分析&#xff09;在智能制造中具有重要的应用价值&#xff0c;但同时也面临着一系列的挑战。 应用&#xff1a; 产品设计优化 在…

react18+react-transition-group实现路由切换过度

效果如下 官网安装对应的插件 创建对应的样式 .fade-enter {opacity: 0; } .fade-exit {opacity: 1; } .fade-enter-active {opacity: 1; } .fade-exit-active {opacity: 0; } .fade-enter-active, .fade-exit-active {transition: opacity 500ms; }const location useLoca…

WSL(Windows Subsystem for Linux)——简单的双系统开发

文章目录 WSLWSL的作用WSL的使用WSL的安装挂载磁盘的作用安装linux发行版wsl下载mysql&#xff0c;mongodb&#xff0c;redis WSL 前言&#xff1a;本人由于在开发中需要linux环境&#xff0c;同时还想要直接在Windows下开发&#xff0c;来提升开发效率&#xff0c;随即简单学…

【问题分析】使用gperftools分析排查内存问题

背景 当程序长时间允许时(压测、服务器程序)&#xff0c;就会面临更大的挑战&#xff0c;其中内存泄漏就是一类典型的问题&#xff0c;内存泄漏往往不易发现&#xff0c;导致的现象更是千奇百怪&#xff0c;本文主要介绍如何借助gperftools分析一个模块的内存泄漏 案例代码 …

SpringBoot框架在高校竞赛管理中的创新应用

3系统分析 3.1可行性分析 通过对本高校学科竞赛平台实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本高校学科竞赛平台采用SSM框架&#xff0c;JAVA作为开发语…

编译/引导EDK2 树莓派4

格蠹的幽兰代码本(RK3588)支持UEFI启动&#xff0c;在阅读RK3588代码的时候发现EDK2也对树莓派系列进行了支持。经过一番尝试&#xff0c;借助幽兰&#xff0c;我也在树莓派上bringup EFI bios(只能引导到Bios setup界面&#xff0c;不知道如何安装OS)&#xff0c;在此记录SOP。…

1.Label Studio 介绍

Label Studio 介绍 文章目录 Label Studio 介绍前言一、安装介绍二、Run with Docker Compose1、WSL2安装2、Docker Desktop安装3、Label Studio安装&#xff08;第二种方法 Run with Docker Compose &#xff09; 三、Install for local development1.下载源码2.安装poetry3.安…

YOLO11改进 | 注意力机制 | 用于增强小目标感受野的RFEM

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 近年来&#xff0c;基于深度学习的人脸检…

【计算机网络】计算机网络相关术语

文章目录 NAT概述NAT的基本概念NAT的工作原理1. **基本NAT&#xff08;静态NAT&#xff09;**2. **动态NAT**3. **NAPT&#xff08;网络地址端口转换&#xff0c;也称为PAT&#xff09;** 底层实现原理1. **数据包处理**2. **转换表**3. **超时机制** NAT的优点NAT的缺点总结 P…

vue3 高德地图标注(飞线,呼吸点)效果

装下这两个 npm 忘了具体命令了&#xff0c;百度一下就行 “loca”: “^1.0.1”, “amap/amap-jsapi-loader”: “^1.0.1”, <template><div id"map" style"width: 100%;height: 100%;"></div> </template><script setup> …