4.cuda全局内存

4. cuda 全局内存

本章目的:剖析核函数与全局内存的联系以及对性能的影响。

4.1 CUDA内存模型概述

目的:在现有的硬件存储子系统下,必须依靠内存模型获得最佳的延迟与带宽。

4.1.1 内存层次结构的优点

应用程序往往遵循局部性原则,这表明他们可以在任意时间点访问相对较小的局部地址空间。有两种不同类型的局部性:

  1. 时间局部性:如果一个数据位置被引用的话,则该数据可能在较短的时间周期内会再次被引用。随着时间退役,则数据被引用的可能性会逐步降低。
  2. 空间局部性:如果一个内存位置被引用,则附近的位置也可能会被引用。

内存结构分布,自顶而下:

  1. 寄存器
  2. 缓存
  3. 主存
  4. 磁盘存储器

容量依次增长,价格依次降低。

cpu与gpu的主存采用的时DRAM(动态随机存取存储器),而低延迟内存(如cpu一级缓存) 采用的时SRAM(静态随机存取存储器)。

4.1.2 CUDA内存模型

对于程序员来讲的话,通常会有两种类型存储器

  1. 可编程的:你需要显式的控制拿写数据存放在可编程内存中。
  2. 不可编程的:你不能决定数据的存储位置,程序将自动决定数据存放的位置已获得良好的性能。

在cpu内存层次结构中,一级缓存和二级缓存都是不可编程的存储器。另一方面,CUDA内存模型提出了多种可编程内存的类型:

  1. 寄存器
  2. 共享内存
  3. 本地内存
  4. 常量内存
  5. 纹理内存
  6. 全局内存
    在这里插入图片描述
4.1.2.4 常量内存

修饰符:

__constant__
常量内存拷贝
cudaError_t cudaMemecpyTosymbol(const void* symbol,const void* src,size_t count);
将count个字节从src指向的内存复制到symbol中。
#include <iostream>
#include <cuda_runtime.h>

#define N 5

// 定义 __constant__ 常量内存数组
__constant__ int const_data[N];

// kernel 访问常量内存
__global__ void readConstantKernel(int* out) {
    int idx = threadIdx.x;
    if (idx < N) {
        out[idx] = const_data[idx];  // 从常量内存中读取
    }
}

int main() {
    int h_data[N] = {1, 2, 3, 4, 5};

    // 将主机数据拷贝到 __constant__ 内存中
    cudaMemcpyToSymbol(const_data, h_data, sizeof(int) * N);

    // 为输出数据分配设备内存
    int* d_out;
    cudaMalloc(&d_out, sizeof(int) * N);

    // 启动 kernel
    readConstantKernel<<<1, N>>>(d_out);

    // 拷贝结果回主机
    int h_out[N];
    cudaMemcpy(h_out, d_out, sizeof(int) * N, cudaMemcpyDeviceToHost);

    // 打印结果
    std::cout << "从 __constant__ 内存读取的数据: ";
    for (int i = 0; i < N; ++i) {
        std::cout << h_out[i] << " ";
    }
    std::cout << std::endl;

    // 清理
    cudaFree(d_out);
    return 0;
}

4.1.2.5 全局内存

是gpu中最大,延迟最高,并且最常使用的内存。global指的是其作用域和生命周期。它的声明可以在任何SM设备中被访问到,并且贯穿应用程序的整个声明周期。

__device__ 
#include<iostream>
#include<stdio.h>
#include<cuda_runtime.h>
__device__ float static_global_data;
__global__ void checkGlobalData() {
    printf("enter it : %f\n",static_global_data);
    // std::cout << "enter it :" << static_global_data << std::endl;
    static_global_data += 2.0f;
}
int main() {
    float value = 3.14f;
    cudaMemcpyToSymbol(static_global_data,&value,sizeof(float));
    std::cout << "host copy to global varaiable :" << value << std::endl;
    checkGlobalData<<<1,1>>>();
    cudaMemcpyFromSymbol(&value,static_global_data,sizeof(float));
    std::cout << "change :host copy to global varaiable :" << value << std::endl;
    cudaDeviceReset();
    return 0;
}
host copy to global varaiable :3.14
enter it : 3.140000
change :host copy to global varaiable :5.14

4.2 内存管理

4.2.1 内存分配和释放

cudaError_t cudaMalloc(void** devptr,size_t count)
cudaError_t cudaMemset(void* devptr,int value,size_t count);
cudaError_t cudaFree(void* ptr);

4.2.2 内存传输

cudaError_t cudaMemcpy(void* dst,const void* src,size_t count,enum cudaMemcpyKind kind);
enum:{
  cudaMemcpyHostToHost
  cudaMemcpyHostToDevice
  cudaMemcpyDeviceToHost
  cudaMemcpyDeviceToDevice
}
#include<iostream>
#include<cuda_runtime.h>
int main(int argc,char** argv) {
    int dev = 0;
    cudaSetDevice(dev);
    unsigned int issize = 1 << 22;
    unsigned int bytes = issize * sizeof(float);
    cudaDeviceProp deviceProp;
    cudaGetDeviceProperties( &deviceProp,dev);
    std::cout << "starting at :" << argv[0] << std::endl;
    float* h_a;
    h_a = (float*) malloc(sizeof(float) * issize);
    float* d_a;
    cudaMalloc((float**) &d_a , bytes);
    for(int i = 0; i < issize; ++i) {
        h_a[i] = 0.5f;
    }
    cudaMemcpy( d_a, h_a, bytes , cudaMemcpyHostToDevice);
    cudaMemcpy( h_a, d_a, bytes , cudaMemcpyDeviceToHost);
    cudaFree(d_a);
    free(h_a);
    cudaDeviceReset();
    return 0;
}

结论:尽可能的减少主机与设备之间的传输。

4.2.3 固定内存

存在的意义:gpu不能在可分页主机内存上安全地访问数据,因为当主机操作系统在物理位置上移动该数据时,它无法控制。当从可分页主机内存传输数据到设备内存中时,cuda驱动程序首先分配临时页面锁定的或固定的主机内存,将主机源数据复制到固定内存中时,然后从固定内存中传输数据给设备内存。

分配固定主机内存函数:

cudaError_t cudaMallocHost(void** devptr,size_t count);
cudaError_t cudaFreeHost(void* ptr);

why?

  1. 分配和释放成本更高。但是它为大规模数据传输提供了更高的传输吞吐量。
  2. 相对于分页内存来看,使用固定内存可以获得加速。
  3. 减少单位传输消耗。
  4. 主机和设备之间可能于内核执行重叠。

4.2.4 零拷贝内存

原理:

统一内存虚拟寻址方式(UVA). 通过这种方式,其有cudaHostAlloc 函数分配的固定主机内存具有相同的主机和设备指针。 然后使用cudaHostGetDevicePointer 函数将返回的指针直接应用于核函数

GPU线程可以直接访问零拷贝内存。在cuda核函数中使用零拷贝内存有以下几个优势:

  1. 当设备内存不足时可利用主机内存。
  2. 避免主机和设备间的显式数据传输。
  3. 提高pcle 传输率。

零拷贝内存时固定(不可分页) 内存。该内存映射到设备地址空间中。****

cudaError_t cudaHostAlloc(void** pHost,size_t count,unsigned int flags);
flags:
	cudaHostAllocDefault == cudaMallocHost
	cudaHostAllocPortable 可以返回能被所有cuda上下文使用的固定内存,而不仅时执行内存分配的哪一个
	cudaHostAllocWriteCombined  该内存通过设备使用映射的固定内存或主机到设备的传输
	cudaHostAllocMapped	可以实现主机写入和设备读取被映射到设备地址空间中的主机内存。
	
如何获取映射到固定内存的设备指针:
cudaError_t cudaHostGetDevicePointer(void** pDevice,void* pHost,unsigned int flags)

#include<stdio.h>
#include<cuda_runtime.h>
#include<sys/time.h>
void init_data(float* data,int size) {
    for(int i = 0; i < size; ++i) {
        data[i] = i * 1.1;
    }
}
void sumArrayOnHost(float* a, float* b,float* c ,size_t nelem) {
    for(int i = 0; i < nelem; ++i) {
        c[i] = a[i] + b[i];
    }
}
double cpuSecond() {
    struct timeval tp;
    gettimeofday(&tp,NULL);
    return ((double)tp.tv_sec + (double)tp.tv_usec*1e-6);

}
__global__ void sumOnArray(float* a,float*b, float*c , int n_elem) {
     int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if(idx < n_elem) {
        c[idx] = a[idx] + b[idx];
    }
}
bool checkResult(float* a, float* b, int n_elem) {
    for(int i = 0; i < n_elem; ++i) {
        if(a[i] != b[i]) {
            return false;
        }
    }
    return true;
}
int main() {
    int dev = 0;
    cudaSetDevice(dev);
    cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop ,dev);
    if(!prop.canMapHostMemory) {
        printf("%ddevice can not support mapping cpu host memeory\n",dev);
        cudaDeviceReset();
        exit(EXIT_SUCCESS);
    }
    printf("using Devvie %d \t: %s\n",dev,prop.name);
    int nelem = 1 << 24;
    size_t n_bytes = sizeof(float) * nelem;
    float* h_a ,*h_b,*hostRef,*gpuRef;
    double start = cpuSecond();
    h_a = (float*) malloc(n_bytes);
    h_b = (float*) malloc(n_bytes);
    hostRef = (float*) malloc(n_bytes);
    gpuRef = (float*) malloc(n_bytes);
    init_data(h_a,nelem);
    init_data(h_b,nelem);
    memset(hostRef,0,n_bytes);
    memset(gpuRef,0,n_bytes);
    sumArrayOnHost(h_a,h_b,hostRef,nelem);
    float* d_a,*d_b,*d_c;
    
    cudaMalloc((float**)&d_a ,n_bytes );
    cudaMalloc((float**)&d_b ,n_bytes );
    cudaMalloc((float**)&d_c ,n_bytes );
    cudaMemcpy(d_a ,h_a ,n_bytes , cudaMemcpyHostToDevice);
    cudaMemcpy(d_b ,h_b ,n_bytes , cudaMemcpyHostToDevice);
    int iLen = 512;
    dim3 block(iLen);
    dim3 grid((nelem + block.x - 1) / block.x);
    sumOnArray<<<grid,block>>>(d_a,d_b,d_c,nelem);
    cudaMemcpy( gpuRef,d_c ,n_bytes , cudaMemcpyDeviceToHost);
    printf("check result is %s\n", checkResult(gpuRef, hostRef, nelem) ? "True" : "False");
    cudaFree(d_a);
    cudaFree(d_b);
    free(h_a);
    free(h_b);
    double end = cpuSecond();
    printf("as usually cost %f ms\n",end-start);
    start = cpuSecond();
    unsigned int flags = cudaHostAllocMapped;
    cudaHostAlloc((void**)&h_a ,n_bytes ,flags);
    cudaHostAlloc((void**)&h_b ,n_bytes ,flags);
    memset(hostRef,0,n_bytes);
    memset(gpuRef,0,n_bytes);
    cudaHostGetDevicePointer((void**)&d_a,(void*)h_a,0);
    cudaHostGetDevicePointer((void**)&d_b,(void*)h_b,0);
    sumArrayOnHost(h_a,h_b,hostRef,nelem);
    sumOnArray<<<grid,block>>>(d_a,d_b,d_c,nelem);
    cudaMemcpy(gpuRef ,d_c ,n_bytes , cudaMemcpyDeviceToHost);
    printf("check result is %s\n", checkResult(gpuRef, hostRef, nelem) ? "True" : "False");
    cudaFree(d_c);
    cudaFreeHost(h_a);
    cudaFreeHost(h_b);
    free(hostRef);
    free(gpuRef);
    cudaDeviceReset();
    end = cpuSecond();
    printf("use zero copy memeory cost %f ms\n",end-start);
    return 0;
}

4.3 内存访问模式

cuda执行的显著特征之一是**指令必须以线程束为单位进行发布与执行。存储操作也是同样。**在执行内存指令时,线程束中的每个线程都提供了一个正在加载或存储的内存地址。在线程束的32 个线程中,每个线程都提出了一个包含请求地址的单一内存访问请求,它并由一个或多个设备内存传输提供服务。以下是几种内存访问的模式。

4.3.1 对齐与合并访问

全局内存通过缓存来实现加载/存储。全局内存是一个逻辑内存空间,可通过核函数来访问它。数据最初存在DRAM(物理设备内存上)。核函数的内存请求通常是在DRAM设备和片上内存间以128字节或32 字节内存事务中实现的。

特性:若只通过二级缓存的话,则这个内存访问是由一个32字节的内存事务实现的。若两级缓存都被用到的话,则是128字节。

特性L1 CacheL2 Cache
位置每个 SM(Streaming Multiprocessor)独立拥有所有 SM 共享的全局缓存
访问范围仅限当前 SM 中的线程访问所有 SM 都能访问(跨 SM 共享)
容量一般 48 KB ~ 128 KB,可与共享内存共享配置几 MB(如 4MB ~ 40MB,视 GPU 而定)
延迟极低(几十个周期)中等(约 200~300 周期)
带宽极高,受 SM 内部总线限制次高,连接所有 SM 的 crossbar
一致性(Coherency)各 SM 之间 不保证一致性L2 是全局一致的(L2 coherence)
缓存粒度一般按 32B 或 128B 行进行缓存一般按 128B 缓存行
用途加速局部数据访问、重复访问数据缓冲显存访问、跨 SM 数据共享
可配置性部分架构可调共享内存:L1 比例(如 64:64 或 32:96)不可配置,由硬件固定

为啥L2 cache 是32字节呢?为啥用到L1 cache的时候就是128呢?

这与 cache line 大小和 warp 合并机制 有关:

层级Cache Line对应机制
L1 Cache128 字节warp 合并访问时,每 32 线程的访问被合并为 128B 对齐的事务(对齐到 128B 边界)
L2 Cache32 字节L2 内部使用更细粒度(32B line)以减少带宽浪费,提高命中率

原因:

  • L1 Cache 直接面对 warp 内部访问,128B 对齐能匹配 warp(32 线程 × 4B = 128B)一次性取数;
  • L2 Cache 面对多个 SM 的并发请求,为了减少带宽浪费,采用更小的 32B 粒度,提升灵活性;
  • 当 L1 被禁用或 bypass 时(例如通过 -Xptxas -dlcm=cg),加载会直接走 L2 → 寄存器,事务大小就变成 32B。
    在这里插入图片描述

合并内存访问:

理想状态:线程束从对其地址开始访问一个连续的内存块。提高带宽的利用率,否则会造成带宽的浪费。

4.3.2 全局内存的读取

三种方式:

  1. 一级与二级缓存(默认的方式)

    但一级缓存的使用取决于两个条件(设备的计算能力、编译器选项(-Xptxas -dlcm=cg 禁用标志 -Xptxas -dlcm=cg 启用标志)),如果禁用的话,则使用二级缓存,如果二级缓存缺失,则就是DRAM,如上面那张图。

  2. 常量缓存

  3. 只读缓存

内存加载访问模式:

  1. 缓存加载(有一级缓存)

  2. 非缓存加载(无一级缓存)

    访问模式__ldca (L1+L2)__ldcg (L2 only)__ldg (readonly)
    连续访问✅ 最快❌ 稍慢✅ 接近
    随机访问❌ 最慢✅ 快✅ 快
    只读数据❌ 容易污染 L1✅ 中等✅ 最佳
float val1 = __ldca(&data[i]); // L1+L2
float val2 = __ldcg(&data[i]); // 仅L2
float val3 = __ldg(&data[i]);  // 只读缓存

上面代码是不同访存模式的方法,其实也可以通过编译器选项(-Xptxas -dlcm=cg 禁用标志 -Xptxas -dlcm=cg 启用标志) 支持。看需求吧。

4.3.4 结构体数组(AoS)与数组结构体(SoA)

struct innerstruct {
  float x;
  float y;
};
struct innerstruct myAos[size]; //AoS
//SoA
struct innerarray {
	float x[n];
	float y[n];
};
#include<iostream>
#include<vector>
#include<cuda_runtime.h>
#include<cstdlib>
#include<ctime>
#include<sys/time.h>
//nvcc aos_gpu.cu -o aos_gpu -std=c++11 -O3
#define LEN 1<<25
struct inner_struct {
    float x;
    float y;
};
double cpuSecond() {
    struct timeval tp;
    gettimeofday(&tp,NULL);
    return ((double)tp.tv_sec + (double)tp.tv_usec*1e-6);

}
void init_inner_struct(struct inner_struct* in, const int& size) {
    for(int i = 0; i < size; ++i) {
        in[i].x = (float)(rand() & 0xFF) / 100.0f;
        in[i].y = (float)(rand() & 0xFF) / 100.0f;
    }
}
__global__ void test_inner_struct_gpu(inner_struct* in, inner_struct* out,const int n) {
    int inx = blockIdx.x * blockDim.x + threadIdx.x;
    if(inx < n) {
        inner_struct tmp = in[inx];
        tmp.x += 10.0f;
        tmp.y += 20.0f;
        out[inx] = tmp;
    }
}
void test_inner_struct_cpu(inner_struct* in, inner_struct* out,const int n) {
    for(int i = 0; i < n; ++i) {
        inner_struct tmp = in[i];
        tmp.x += 10.0f;
        tmp.y += 20.0f;
        out[i] = tmp;
    }
}
bool check(const inner_struct* gpu_ref, const inner_struct* cpu_ref,const int n) {
    for(int i = 0; i < n; ++i) {
        if(std::abs(gpu_ref[i].x - cpu_ref[i].x) > 1e-6 || 
           std::abs(gpu_ref[i].y - cpu_ref[i].y) > 1e-6) {
            return false;
        }
    }
    return true;
}
int main(int argc,char** argv) {
    int dev = 0;
    cudaDeviceProp deviceProp;
    cudaGetDeviceProperties(&deviceProp,dev);
    std::cout << argv[0] << "test struct array at \t" << "device " << dev << "\t " << deviceProp.name << std::endl;
    cudaSetDevice(dev);
    int n_elem = LEN;
    size_t n_bytes = sizeof(inner_struct) * n_elem;
    inner_struct* h_a = (inner_struct*) malloc(n_bytes);
    inner_struct* host_ref = (inner_struct*) malloc(n_bytes);
    inner_struct* gpu_ref = (inner_struct*) malloc(n_bytes);
    init_inner_struct(h_a,n_elem);
    double start = cpuSecond();
    test_inner_struct_cpu(h_a,host_ref,n_elem);
    double cost = cpuSecond() - start;
    std::cout << "cpu cost:" << cost <<  "ms" << std::endl;
    inner_struct* d_a, *d_c;
    cudaMalloc((inner_struct**)&d_a,n_bytes);
    cudaMalloc((inner_struct**)&d_c,n_bytes);
    cudaMemcpy(d_a,h_a,n_bytes,cudaMemcpyHostToDevice);

    int block_size = 128;
    if(argc > 1) {
        block_size = atoi(argv[1]);
    }

    dim3 block(block_size,1);
    dim3 grid((n_elem + block.x - 1) / block.x ,1);
    start = cpuSecond();
    test_inner_struct_gpu<<<grid,block>>>(d_a,d_c,n_elem);
    cudaDeviceSynchronize();
    cost = cpuSecond() - start;
    std::cout << "gpu cost:" << cost <<  "ms" << std::endl;
    cudaMemcpy(gpu_ref,d_c,n_bytes,cudaMemcpyDeviceToHost);
    bool result = check(gpu_ref,host_ref,n_elem);
    std::cout << "check result :" << result << std::endl;
    free(h_a);
    free(host_ref);
    free(gpu_ref);
    cudaFree(d_a);
    cudaFree(d_c);
    cudaDeviceReset();
    return 0;

}

那个soa不写了,直接讲结论吧:这两种模式下,最好使用soa(结构体数组)。因为其内存更连续,访问效率更快,

4.3.5 性能调整

如何提高内存带宽利用率:

  1. 对齐及合并内存访问,减少带宽的浪费。
    展开技术:比如我现在要做一个,那我可不可以一次循环做多次加法,比如原先是每次调用核函数算一个,我现在改用每次算4个。将其作成一个计算密集型的操作,以此来加速。

     before:
    __global__ void add (float* a ,float* n,float*c ,int n)
      int i = blockIdx.x * blockDim.x + threadIdx.x;
      if(i < n) {
      	c[i] = a[i] + b[i];
      }
      after:
      __global__ void add(float* a, float* b,float *c,int n ) {
      	int i = blockIdx.x * blockDim.x * 4 + threadIdx.x;
      	if(i < n) {
      	   c[i] =  a[i] + b[i];
      	   c[i + blockDim.x] = a[i + blockDim.x ] + b[i + blockDim.x ];
      	   c[i +2 *  blockDim.x] = a[i +2* blockDim.x ] + b[i + 2* blockDim.x ];
      	   c[i +3 *  blockDim.x] = a[i +3 *  blockDim.x ] + b[i + 3* blockDim.x ];
      	}
      }
    
  2. 足够的并发内存操作。
    给足够大的线程数与网格数,但要是内存对齐的。这个得需要自己实验。

4.4 矩阵的转置

#include<iostream>
#include<cuda_runtime.h>
#define N 5 //行数
#define M 6 //列数
void print(const float* data, int size) {
    for(int i = 0; i < size; ++i) {
        std::cout << data[i] << "\t";
    }
    std::cout << std::endl;
}
__global__ void transpose(float* out,const float* in,int rows,int cols) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;

    if(row < rows && col < cols) {
        // out[rows * col + row] = in[row * cols + col]; 
        out[col * rows + row] = in[row * cols + col];
    } 
}
int main() {
    const int size = N * M * sizeof(float);
    float h_in[N * M] , h_out[N * M];
    for(int i = 0; i < size; ++i) {
        h_in[i] = static_cast<float>(i);
    }
    std::cout << "before transpose" << std::endl;
    for(int i = 0; i < N; ++i) {
        for(int j = 0; j < M; ++j) {
            std::cout << h_in[i * M + j] << "\t";
        }
        std::cout << std::endl;
    }
    // print(h_in,N * M);
    float* d_in;
    float* d_out;
    cudaMalloc(&d_in,size);
    cudaMalloc(&d_out,size);
    dim3 blockDim(16,16);
    dim3 blockGrid((M + blockDim.x - 1) / blockDim.x ,(N + blockDim.y - 1) / blockDim.y );
    cudaMemcpy(d_in ,h_in ,size , cudaMemcpyHostToDevice);
    transpose<<<blockGrid,blockDim>>>(d_out,d_in,N,M);
    cudaMemcpy( h_out,d_out , size , cudaMemcpyDeviceToHost);
    std::cout << "after transpose" << std::endl;
    for(int i = 0 ; i < M; ++i) {
        for(int j = 0; j < N ; ++j) {
            std::cout << h_out[i * N + j] << "\t";
        }
        std::cout << std::endl;
    }
    cudaFree(d_in);
    cudaFree(d_out);
    return 0;
}

再补一个使用shared_memory的版本

#include <iostream>
#include <cuda_runtime.h>

#define N 4  // 行数
#define M 5  // 列数
//warp 是 GPU 中最小的执行单元,通常是 32 个线程组成的线程束(thread bundle)。

// shared memory 被分成多个 bank(银行),每个 bank 可同时被一个线程访问。
// CUDA 的共享内存由多个 bank 组成(在大多数现代 GPU 中是 32 个 bank)
// 如果多个线程(如一个 warp 的多个线程)同时访问 不同 bank,可以并行访问
// 如果多个线程访问 同一个 bank 的同一个地址,没事(只读)
// 如果多个线程访问 同一个 bank 但不同地址,就会发生 bank conflict(冲突),要串行执行,性能下降

// BLOCK_SIZE 通常设置为 16 或 32 以适配 warp 和 bank
//memory bank 概念

// CUDA 的共享内存被划分为多个称为 memory banks(内存银行) 的单元。通常每个 warp 中的 32 个线程 访问共享内存时,每个线程访问一个 bank,且不同线程访问不同 bank,访问可并行执行(无冲突)。

// 一个 bank 能同时响应一个访问请求;

// 如果多个线程访问 同一个 bank 不同地址,就会发生 bank conflict;
// 无 bank conflict:

// __shared__ float tile[32][32];
// float val = tile[threadIdx.y][threadIdx.x];
// 假设 threadIdx.x 在 0~31 之间,每个线程访问的 tile[threadIdx.y][threadIdx.x] 映射到不同 bank,无冲突。
// ❌ 有 bank conflict:
// __shared__ float tile[32][32];
// float val = tile[threadIdx.x][0]; // 所有线程访问同一列
// 这里 32 个线程访问的是 tile[0][0], tile[1][0], ..., tile[31][0],可能全部落在同一个 bank 上,会形成冲突。
// 这时访问将会被 串行执行,性能大大降低。
#define BLOCK_SIZE 16

__global__ void transposeSharedMemory(float* out, const float* in, int rows, int cols) {
    __shared__ float tile[BLOCK_SIZE][BLOCK_SIZE + 1]; // +1 避免 bank conflict

    int x = blockIdx.x * BLOCK_SIZE + threadIdx.x; // 列索引
    int y = blockIdx.y * BLOCK_SIZE + threadIdx.y; // 行索引

    if (x < cols && y < rows) {
        int inputIdx = y * cols + x;
        tile[threadIdx.y][threadIdx.x] = in[inputIdx];
    }

    __syncthreads(); // 保证 shared memory 中 tile 完全写入后再读出

    // 重新计算 transposed 后的位置
    int transposed_x = blockIdx.y * BLOCK_SIZE + threadIdx.x;
    int transposed_y = blockIdx.x * BLOCK_SIZE + threadIdx.y;

    if (transposed_x < rows && transposed_y < cols) {
        int outputIdx = transposed_y * rows + transposed_x;
        out[outputIdx] = tile[threadIdx.x][threadIdx.y];
    }
}
//  避免 bank conflict 的技巧
// ✅ 方法:使用 padding
// __shared__ float tile[32][33]; // +1 padding 列
// 使得每一行的起始地址跨越 bank 边界,从而让 tile[threadIdx.y][threadIdx.x] 在转置时依然避免冲突。

// 这就是你之前看到的:
int main() {
    const int size = N * M * sizeof(float);

    // 初始化输入矩阵
    float h_in[N * M], h_out[M * N];
    for (int i = 0; i < N * M; i++) {
        h_in[i] = static_cast<float>(i);
    }

    // 分配设备内存
    float *d_in, *d_out;
    cudaMalloc(&d_in, size);
    cudaMalloc(&d_out, size);

    cudaMemcpy(d_in, h_in, size, cudaMemcpyHostToDevice);

    // 启动 shared memory 优化 kernel
    dim3 blockDim(BLOCK_SIZE, BLOCK_SIZE);
    dim3 gridDim((M + BLOCK_SIZE - 1) / BLOCK_SIZE, (N + BLOCK_SIZE - 1) / BLOCK_SIZE);
    transposeSharedMemory<<<gridDim, blockDim>>>(d_out, d_in, N, M);

    cudaMemcpy(h_out, d_out, size, cudaMemcpyDeviceToHost);

    // 打印转置后的矩阵
    std::cout << "Transposed matrix:\n";
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            std::cout << h_out[i * N + j] << "\t";
        }
        std::cout << "\n";
    }

    // 清理
    cudaFree(d_in);
    cudaFree(d_out);

    return 0;
}
<think>好的,我现在需要帮助用户理解CUDA中显存和全局内存的关系及使用方法。首先,我得回顾一下自己的知识,确保准确性。 用户提到的显存和全局内存,在CUDA中可能有不同的定义。显存通常指的是GPU上的设备内存,而全局内存可能属于显存的一部分。我需要明确这两者的关系。根据引用[1]和引用[2],全局内存是显存的一部分,由所有线程块共享,访问速度较慢。而显存还包含其他类型的内存,如共享内存、常量内存等。 接下来,要区分两者的使用方式。显存分配通常使用cudaMalloc,如引用[3]中的例子,分配的是全局内存全局内存的使用需要注意对齐和访问模式,例如合并访问,如引用[2]提到的,通过共享内存来优化全局内存访问。同时,引用[1]指出cudaMalloc分配的内存地址是256字节对齐的,这对性能很重要。 用户可能还关心两者的区别,比如作用域和速度。全局内存对所有线程可见,但延迟高;而共享内存仅限线程块内,速度更快。需要举例说明,比如矩阵乘法的优化,使用共享内存减少全局内存访问次数。 还要考虑用户的实际应用场景,比如何时使用全局内存,何时优化。可能的问题包括数据传输、原子操作等,如引用[3]中的原子函数使用全局内存。需要强调正确使用内存类型对性能的影响。 最后,要确保回答结构清晰,分点说明关系和区别,并给出使用建议。同时生成相关问题,帮助用户深入理解。</think>### CUDA中显存与全局内存的关系及使用方法解析 #### 一、基本概念与关系 1. **显存(Device Memory)** - 指GPU板载的物理存储空间,包括多种类型的内存:**全局内存(Global Memory)**、常量内存(Constant Memory)、纹理内存(Texture Memory)、本地内存(Local Memory)等[^2]。 - 显存通过`cudaMalloc`分配,需手动管理生命周期(如`cudaFree`释放)。 2. **全局内存(Global Memory)** - 是显存中**容量最大、访问延迟最高**的部分,所有线程均可访问,作用域为整个GPU[^2]。 - 通常用于存储大规模输入/输出数据,如数组、图像等。 #### 二、核心区别 | 特性 | 全局内存 | 显存(整体) | |-------------------|----------------------------|---------------------------| | 物理归属 | GPU显存的一部分 | GPU板载物理存储 | | 作用域 | 所有线程块和线程可见 | 包含多种内存类型 | | 访问速度 | 较慢(需通过L2缓存) | 不同类型速度差异大 | | 控制方式 | 通过CUDA API(如`cudaMalloc`) | 统一由CUDA内存模型管理 | #### 三、全局内存的使用要点 1. **分配与释放** ```c int *d_data; cudaMalloc(&d_data, size); // 分配全局内存 cudaFree(d_data); // 释放 ``` - 分配时需保证内存对齐(如256字节对齐)以优化访问性能[^1]。 2. **访问优化** - **合并访问(Coalesced Access)**:相邻线程应访问相邻内存地址,减少事务次数。例如: ```c __global__ void kernel(float *output, float *input) { int tid = blockIdx.x * blockDim.x + threadIdx.x; output[tid] = input[tid]; // 连续访问,满足合并条件 } ``` - **利用共享内存**:先将全局数据加载到共享内存(更快),处理后再写回[^2]。 3. **原子操作** - 用于多线程对全局变量的安全修改(如累加): ```c __global__ void atomic_add(int *counter) { atomicAdd(counter, 1); // 原子操作保证线程安全 } ``` 引用示例见[^3]。 #### 四、典型应用场景 1. **大规模数据处理** - 图像处理(如卷积)、科学计算(如矩阵运算)。 2. **跨线程块通信** - 通过全局内存实现线程块间的数据交互(需同步机制)。 3. **与主机内存协作** - 使用`cudaMemcpy`在主机内存和全局内存间传输数据。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值