目录
摘要
本文将介绍常量内存和只读缓存,然后对比其差异
常量内存
常量内存是一种专用的全局内存区域,用于存放只读数据。对于GPU端的内核代码,常量内存是完全只读的,GPU线程只能读取,不能写入。主机端(CPU)可以在内核启动之前,通过**
cudaMemcpyToSymbol**等API向常量内存写入数据,或者在内核执行完毕后读取数据。常量内存是位于片外,而不是片上,片上的是常量缓存,关于常量缓存我们是查不到的,其实对我们也没啥可编程的作用,这个是硬件的作用,不像共享内存一样开放给我们进行编程,这样知道其大小就有用,我们只能知道常量内存的大小
cppcout<<"常量内存大小:"<<prop.totalConstMem<<endl;由此可得常量内存大小是64KB
常量缓存特性
常量内存是存放数据的地方,接下来讲的特性是属于常量缓存的
常量缓存具有单周期广播性质,也就是当请求的地址是同一个的时候,常量缓存能够把数据广播给32个线程
第 1 步:Warp Scheduler 发射指令
Warp Scheduler 从就绪的 Warp 中挑中它,发射一条
LDC(Load Constant)指令给 LSU。第 2 步:LSU 把请求路由给常量缓存
LSU 收到指令后,收集这 32 个线程的地址。但它不做合并(常量内存走的是常量缓存通路,不走 L1),而是直接把这 32 个地址发送给常量缓存控制器。
第 3 步:常量缓存控制器判断是否广播
常量缓存控制器检查这 32 个地址:
如果所有地址都相同 → 触发广播。控制器直接从常量缓存的 SRAM 中取出这份数据,通过专用的广播总线,在一个时钟周期内发送给所有 32 个线程。LSU 负责把数据写入各线程的目标寄存器。
如果地址不同 → 广播失效。控制器只能串行化处理这些请求,一个接一个地从常量缓存(或更底层的 DRAM)读取数据。此时性能暴跌,甚至可能比普通全局内存还慢。
第 4 步:如果常量缓存缺失
如果请求的数据不在常量缓存的SRAM 里,控制器会把请求转发给 L2 缓存 ,L2 再未命中就从片外 DRAM 的常量内存区域(那 64KB)搬来整个 128 字节 Cache Line。数据进入常量缓存后,再按第 3 步的逻辑返回给 LSU。
注意:一旦这32个线程要的是不同的地址,就会变串行
如果是全局显存,LSU拿到数据之后不是一个周期就能写到32个线程的寄存器的,需要多个周期
分配方式
常量内存必须在所有函数外部 ,以 **
__constant__**关键字声明。它的大小是静态固定的,且必须在主机端通过专用 API 来初始化或更新数据。① 声明常量内存变量
cpp// 全局作用域,文件作用域内可见 __constant__ float weights[256];② 从主机端拷贝数据到常量内存
必须使用
cudaMemcpyToSymbol,不能用普通的cudaMemcpy。
cppfloat h_weights[256]; // ... 初始化 h_weights ... cudaMemcpyToSymbol(weights, h_weights, sizeof(h_weights));为什么不能用
cudaMemcpy? 因为__constant__变量不是普通的指针,它没有可以被cudaMemcpy识别的"设备地址"。cudaMemcpyToSymbol内部会自动处理这种特殊的地址映射。③ 从主机端读取常量内存中的数据
使用
cudaMemcpyFromSymbol,同样不能直接用cudaMemcpy。
cppfloat h_copy[256]; cudaMemcpyFromSymbol(h_copy, weights, sizeof(weights));④ 如果常量变量在多个
.cu文件中共享需要在声明时使用
extern关键字(一个文件定义,其他文件声明):
cpp// file1.cu: 定义 __constant__ float shared_data[1024]; // file2.cu: 声明 extern __constant__ float shared_data[1024];注意:常量内存的生命周期和程序一致,程序结束,常量内存才结束,初始化一次,后面都可以一直用了
内存类型 生命周期 声明方式 常量内存 ( __constant__)整个应用程序(最长) 文件作用域全局声明 全局内存 ( cudaMalloc)由程序员控制(从分配 cudaMalloc到释放cudaFree)动态分配 共享内存 ( __shared__)线程块(Block)的生命周期 核函数内声明 寄存器 / 局部内存 线程的生命周期 核函数内声明局部变量
使用常量内存实现一维模板
相信人工智能发达的今天,大家都听过卷积,或者上过视觉的或者数值分析等课程,如果没听过的去查一下,很简单,就是一个卷积核模板,一个矩阵,卷积就是把模板依次滑过矩阵
在对应位置相乘再相加
这是数值分析里的一维高阶差分求导模板
那这就是一维高阶求导,d_in是你的数据,然后我们经过模板之后每个位置都用相邻的8个元素包括本身就是9个元素,然后进行差分求导,求出来的数据就是d_out\[\],每个点都有导数
通过以上的背景,我们知道c0c1c2c3是常量,是模板,那么可以使用常量内存,而且此时的输入数据d_in可以使用共享内存,因为每个数据都要被使用8次,所以最好的办法就是一次加载多次使用,那就是共享内存
cpp#include<cuda_runtime.h> #include "../freshman.hpp" using namespace std; #define TEMPLATE_SIZE 9 #define TEMP_RADIO_SIZE (TEMPLATE_SIZE/2) #define BDIM 32 __constant__ float coef[TEMP_RADIO_SIZE]; void convolution(float *in,float *out,float* template_,const unsigned int array_size) { for(int i=TEMP_RADIO_SIZE;i<array_size-TEMP_RADIO_SIZE;i++) { for(int j=1;j<=TEMP_RADIO_SIZE;j++) { out[i]+=template_[j-1]*(in[i+j]-in[i-j]); } //printf("%d:CPU :%lf\n",i,out[i]); } } __global__ void stencil_1d(float * in,float * out){ //block边缘会越界,为了不越界并且能够计算,所以填充前面四个,后面四个,一共8个位置 __shared__ float smem[BDIM+2*TEMP_RADIO_SIZE]; //计算全局索引 int idx=threadIdx.x+blockDim.x*blockIdx.x; //跳过前面四个进行加载数据到共享内存 int sidx=threadIdx.x+TEMP_RADIO_SIZE; smem[sidx]=in[idx]; //此时处理前面四个和后面四个 if (threadIdx.x<TEMP_RADIO_SIZE){ //如果此时的block是中间的,那么前面和后面都有数据直接用 if(idx>TEMP_RADIO_SIZE) smem[sidx-TEMP_RADIO_SIZE]=in[idx-TEMP_RADIO_SIZE]; if(idx<gridDim.x*blockDim.x-BDIM) smem[sidx+BDIM]=in[idx+BDIM]; } //同步保证所有数据都加载到共享内存 __syncthreads(); //全局边缘位置不计算 if (idx<TEMP_RADIO_SIZE||idx>=gridDim.x*blockDim.x-TEMP_RADIO_SIZE) return; float temp=.0f; //计算差分 #pragma unroll for(int i=1;i<=TEMP_RADIO_SIZE;i++) { temp+=coef[i-1]*(smem[sidx+i]-smem[sidx-i]); } out[idx]=temp; //printf("%d:GPU :%lf,\n",idx,temp); } //read only __global__ void stencil_1d_readonly(float * in,float * out,const float* __restrict__ dcoef) { __shared__ float smem[BDIM+2*TEMP_RADIO_SIZE]; int idx=threadIdx.x+blockDim.x*blockIdx.x; int sidx=threadIdx.x+TEMP_RADIO_SIZE; smem[sidx]=in[idx]; if (threadIdx.x<TEMP_RADIO_SIZE) { if(idx>TEMP_RADIO_SIZE) smem[sidx-TEMP_RADIO_SIZE]=in[idx-TEMP_RADIO_SIZE]; if(idx<gridDim.x*blockDim.x-BDIM) smem[sidx+BDIM]=in[idx+BDIM]; } __syncthreads(); if (idx<TEMP_RADIO_SIZE||idx>=gridDim.x*blockDim.x-TEMP_RADIO_SIZE) return; float temp=.0f; #pragma unroll for(int i=1;i<=TEMP_RADIO_SIZE;i++) { temp+=dcoef[i-1]*(smem[sidx+i]-smem[sidx-i]); } out[idx]=temp; //printf("%d:GPU :%lf,\n",idx,temp); } int main(int argc,char** argv){ int dimx=BDIM; unsigned int nxy=1<<16; int nBytes=nxy*sizeof(float); //Malloc float* in_host=(float*)malloc(nBytes); float* out_gpu=(float*)malloc(nBytes); float* out_cpu=(float*)malloc(nBytes); memset(out_cpu,0,nBytes); //initialData(in_host,nxy); //cudaMalloc float *in_dev=NULL; float *out_dev=NULL; initialData(in_host,nxy); float templ_[]={-1.0,-2.0,2.0,1.0}; CHECK(cudaMemcpyToSymbol(coef,templ_,TEMP_RADIO_SIZE*sizeof(float))); CHECK(cudaMalloc((void**)&in_dev,nBytes)); CHECK(cudaMalloc((void**)&out_dev,nBytes)); CHECK(cudaMemcpy(in_dev,in_host,nBytes,cudaMemcpyHostToDevice)); CHECK(cudaMemset(out_dev,0,nBytes)); // cpu compute double iStart=efficiency(); convolution(in_host,out_cpu,templ_,nxy); double iElaps=efficiency()-iStart; //printf("CPU Execution Time elapsed %f sec\n",iElaps); // stencil 1d dim3 block(dimx); dim3 grid((nxy-1)/block.x+1); double stencil_1d_start=efficiency(); stencil_1d<<<grid,block>>>(in_dev,out_dev); CHECK(cudaDeviceSynchronize()); double stencil_1d_iElaps=efficiency()-stencil_1d_start; printf("stencil_1d Time elapsed %f sec\n",stencil_1d_iElaps); CHECK(cudaMemcpy(out_gpu,out_dev,nBytes,cudaMemcpyDeviceToHost)); check(out_cpu,out_gpu,nxy); CHECK(cudaMemset(out_dev,0,nBytes)); // stencil 1d read only float * dcoef_ro; CHECK(cudaMalloc((void**)&dcoef_ro,TEMP_RADIO_SIZE * sizeof(float))); CHECK(cudaMemcpy(dcoef_ro,templ_,TEMP_RADIO_SIZE * sizeof(float),cudaMemcpyHostToDevice)); double stencil_1d_readonly_start=efficiency(); stencil_1d_readonly<<<grid,block>>>(in_dev,out_dev,dcoef_ro); CHECK(cudaDeviceSynchronize()); double stencil_1d_readonly_iElaps=efficiency()-stencil_1d_readonly_start; printf("stencil_1d_readonly Time elapsed %f sec\n",stencil_1d_readonly_iElaps); CHECK(cudaMemcpy(out_gpu,out_dev,nBytes,cudaMemcpyDeviceToHost)); check(out_cpu,out_gpu,nxy); cudaFree(dcoef_ro); cudaFree(in_dev); cudaFree(out_dev); free(out_gpu); free(out_cpu); free(in_host); cudaDeviceReset(); return 0; }
只读缓存
只读缓存,在现代 Ada Lovelace 架构中,并不是一个独立的物理硬件,而是 L1 数据缓存的一种"VIP 使用模式"。它的作用是:当你明确告诉编译器"我这块数据在整个核函数运行期间都不会被修改"时,L1 缓存会用更优的策略来缓存这些数据,让它们尽量不被频繁读写的数据挤掉。
分配方式
方式一:使用
__ldg()内联函数(最直接)
__ldg()是 CUDA 提供的一个内置函数,专门用于发起一次只读的全局内存加载 。你不需要改变指针的类型声明,只需在读取数据时用__ldg()包裹一下即可。
cpp__global__ void kernel_readonly(float *output, float *large_table, int n) { int idx = threadIdx.x + blockIdx.x * blockDim.x; if (idx < n) { // 用 __ldg() 显式告诉编译器:这次加载是只读的,请走只读缓存路径 float val = __ldg(&large_table[idx]); output[idx] = val * 2.0f; } }关键点 :
__ldg()作用于每次具体的加载操作,你可以只对关键的只读数据使用它,其他读写数据仍然走普通路径。方式二:使用
const __restrict__指针(更省心)在核函数的参数声明中,给只读指针加上
const __restrict__修饰。编译器会自动把符合条件的内存访问映射到只读缓存路径。
cpp// 注意参数中的 const __restrict__ __global__ void kernel_readonly(float *output, const float* __restrict__ large_table, int n) { int idx = threadIdx.x + blockIdx.x * blockDim.x; if (idx < n) { // 不需要 __ldg(),编译器自动走只读缓存路径 float val = large_table[idx]; output[idx] = val * 2.0f; } }关键点 :
const表示数据不可修改,__restrict__告诉编译器这个指针是访问这块内存的唯一途径(没有别名),编译器因此可以放心地把所有对large_table的读取都映射到只读缓存路径。
实验
cpp//read only __global__ void stencil_1d_readonly(float * in,float * out,const float* __restrict__ dcoef) { __shared__ float smem[BDIM+2*TEMP_RADIO_SIZE]; int idx=threadIdx.x+blockDim.x*blockIdx.x; int sidx=threadIdx.x+TEMP_RADIO_SIZE; smem[sidx]=in[idx]; if (threadIdx.x<TEMP_RADIO_SIZE) { if(idx>TEMP_RADIO_SIZE) smem[sidx-TEMP_RADIO_SIZE]=in[idx-TEMP_RADIO_SIZE]; if(idx<gridDim.x*blockDim.x-BDIM) smem[sidx+BDIM]=in[idx+BDIM]; } __syncthreads(); if (idx<TEMP_RADIO_SIZE||idx>=gridDim.x*blockDim.x-TEMP_RADIO_SIZE) return; float temp=.0f; #pragma unroll for(int i=1;i<=TEMP_RADIO_SIZE;i++) { temp+=dcoef[i-1]*(smem[sidx+i]-smem[sidx-i]); } out[idx]=temp; //printf("%d:GPU :%lf,\n",idx,temp); }两次的数据非常接近,甚至有时候只读的还能跑过常量,可能是跟数据量非常小的问题
但是只读的波动非常大,有时候还很慢
总结
特性 常量内存 (Constant Memory) 只读缓存 (Read-Only Cache) 物理存储 片外 DRAM,但拥有一块独立的片上常量缓存(Ada 架构为 8KB/SM) 数据仍在片外 DRAM,加速路径是L1 数据缓存的一部分(非独立硬件) 触发方式 __constant__声明 +cudaMemcpyToSymbol初始化__ldg()内联函数,或const __restrict__指针核心优势 单周期广播:一个 Warp 内所有线程读同一地址时,一个时钟周期完成 缓存保护:数据被标记为只读,优先保留在 L1 缓存,不被普通读写数据轻易挤掉 最佳访问模式 全 Warp 统一地址(如所有线程读同一个权重) 每个线程读不同地址,但这些地址通常是连续且只读的 容量限制 64KB(整个 GPU) 受 L1 缓存容量和全局内存容量共同限制,远大于 64KB 主机端如何写入 cudaMemcpyToSymbol(专用 API)cudaMemcpy(普通拷贝)常量内存是专为"小、统一、只读"场景打造的广播利器;只读缓存是为"大、各异、只读"场景提供的 L1 保护策略。






