常量内存和只读缓存

目录

摘要

常量内存

常量缓存特性

分配方式

使用常量内存实现一维模板

只读缓存

分配方式

实验

总结


摘要

本文将介绍常量内存和只读缓存,然后对比其差异

常量内存

常量内存是一种专用的全局内存区域,用于存放只读数据。对于GPU端的内核代码,常量内存是完全只读的,GPU线程只能读取,不能写入。主机端(CPU)可以在内核启动之前,通过**cudaMemcpyToSymbol**等API向常量内存写入数据,或者在内核执行完毕后读取数据。

常量内存是位于片外,而不是片上,片上的是常量缓存,关于常量缓存我们是查不到的,其实对我们也没啥可编程的作用,这个是硬件的作用,不像共享内存一样开放给我们进行编程,这样知道其大小就有用,我们只能知道常量内存的大小

cpp 复制代码
cout<<"常量内存大小:"<<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

cpp 复制代码
float h_weights[256];

// ... 初始化 h_weights ...
cudaMemcpyToSymbol(weights, h_weights, sizeof(h_weights));

为什么不能用 cudaMemcpy 因为 __constant__ 变量不是普通的指针,它没有可以被 cudaMemcpy 识别的"设备地址"。cudaMemcpyToSymbol 内部会自动处理这种特殊的地址映射。

③ 从主机端读取常量内存中的数据

使用 cudaMemcpyFromSymbol,同样不能直接用 cudaMemcpy

cpp 复制代码
float 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 保护策略。

相关推荐
一条大祥脚1 小时前
ABC468 扫描线|贡献法|二阶差分|线段树优化DP
数据结构·算法
IT小盘1 小时前
16-Prompt版本管理-从手工修改到可追踪配置系统
开发语言·人工智能·python·prompt
满怀冰雪1 小时前
15-Paddle 高层 API 入门:paddle.Model 的训练与评估流程
人工智能·python·深度学习·机器学习·paddle
钟佩颖1 小时前
尚硅谷AI Coding教程
人工智能
happyprince1 小时前
篇2-bitsandbytes-具体观-算法与实现剖析
算法
u0103055271 小时前
ArrayList操作详解与实战应用
人工智能·1024程序员节
神奇霸王龙1 小时前
MCP v5 Agent Skills 屠夫榜:5 旗舰子代理
网络·人工智能·ai·aigc·agent·mcp·skills
Cx330❀1 小时前
【Linux网络】深入 HTTP 协议(五):从 Cookie/Session 原理到 C++ 源码实战
linux·运维·服务器·开发语言·网络·c++·http
Elastic 中国社区官方博客1 小时前
Elasticsearch:语义搜索快速入门
大数据·人工智能·elasticsearch·搜索引擎·全文检索