CUDA编程指南基础知识点总结(5)

以下是针对 2.2.3 GPU Device Memory Spaces 章节内容的详细知识点整理,包含所有内存类型的特性、使用方法及最佳实践。


2.2.3. GPU Device Memory Spaces (GPU设备内存空间)

内存类型概览表

内存类型 作用域 生命周期 物理位置 关键特性
全局内存 网格(所有线程) 应用程序 设备 容量大,延迟高,所有线程可访问
常量内存 网格(所有线程) 应用程序 设备 只读,缓存优化,延迟低
共享内存 块(块内线程) 内核 SM 用户管理,低延迟,高带宽
局部内存 线程 内核 设备 逻辑线程局部,物理在全局内存
寄存器 线程 内核 SM 最快存储,编译器管理

2.2.3.1. Global Memory (全局内存)

基本概念

  • 别名:设备内存(device memory)
  • 类比:相当于CPU系统中的RAM
  • 作用:内核中所有线程都可以访问的主要存储空间

关键特性

特性 说明
可访问性 网格内的所有线程都可读/写
持久性 分配后持续存在,直到显式释放或程序终止
生命周期 从分配到cudaFree()cudaDeviceReset()

分配与释放API

操作 API 说明
分配 cudaMalloc() 分配设备内存
分配(统一内存) cudaMallocManaged() 分配可由CPU/GPU访问的内存
拷贝 cudaMemcpy() 主机与设备间数据传输
释放 cudaFree() 释放设备内存

使用流程

cpp 复制代码
// 1. 分配全局内存
cudaMalloc(&d_data, size);

// 2. 初始化数据(通过拷贝)
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);

// 3. 内核中使用
kernel<<<grid, block>>>(d_data);

// 4. 将结果拷贝回主机
cudaMemcpy(h_result, d_data, size, cudaMemcpyDeviceToHost);

// 5. 释放内存
cudaFree(d_data);

注意事项

  • 数据竞争:多个线程同时读写同一位置需同步
  • 返回值 :内核是void类型,只能通过全局内存将结果返回主机
  • 持久性:数据在内核执行间持续存在,可被多个内核使用

示例:vecAdd内核

cpp 复制代码
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
{
    int workIndex = threadIdx.x + blockIdx.x * blockDim.x;
    if(workIndex < vectorLength)
    {
        C[workIndex] = A[workIndex] + B[workIndex];
    }
}
  • A、B、C指针指向全局内存
  • 所有线程可同时读取A、B,写入C

2.2.3.2. Shared Memory (共享内存)

基本概念

  • 物理位置:每个SM内部
  • 资源关系:与L1缓存共享同一物理资源
  • 类比:用户管理的暂存器(scratchpad)

关键特性

特性 说明
可访问性 同一线程块内的所有线程
生命周期 内核执行期间
性能 比全局内存带宽更高、延迟更低
容量 较小,依GPU架构而异

同步机制:__syncthreads()

cpp 复制代码
__syncthreads();
  • 作用:阻塞块内所有线程,直到全部到达该调用点
  • 用途:确保所有线程对共享内存的写入完成后再读取
  • 注意:在条件分支中使用需谨慎(可能导致死锁)

同步示例

cpp 复制代码
__global__ void example_syncthreads(int* input_data, int* output_data) {
    __shared__ int shared_data[128];
    
    // 每个线程写入共享内存的不同位置
    shared_data[threadIdx.x] = input_data[threadIdx.x];
    
    // 同步:确保所有写入完成
    __syncthreads();
    
    // 单个线程安全地读取所有共享数据
    if (threadIdx.x == 0) {
        int sum = 0;
        for (int i = 0; i < blockDim.x; ++i) {
            sum += shared_data[i];
        }
        output_data[blockIdx.x] = sum;
    }
}

共享内存大小查询

属性 说明
sharedMemPerMultiprocessor 每个SM的共享内存总量
sharedMemPerBlock 每个线程块可用的最大共享内存
cpp 复制代码
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device);
printf("Shared memory per SM: %zu\n", prop.sharedMemPerMultiprocessor);
printf("Shared memory per block: %zu\n", prop.sharedMemPerBlock);

缓存配置:cudaFuncSetCacheConfig()

cpp 复制代码
cudaFuncSetCacheConfig(kernel, cudaFuncCachePreferShared);
// 选项:
// cudaFuncCachePreferNone   - 无偏好
// cudaFuncCachePreferShared - 偏好更多共享内存
// cudaFuncCachePreferL1     - 偏好更多L1缓存
// cudaFuncCachePreferEqual  - 偏好平衡
  • 注意 :这只是给运行时的提示,不保证一定采用
  • 运行时根据资源和内核需求自由决定

2.2.3.2.1. Static Allocation (静态分配)

语法
cpp 复制代码
__shared__ float sharedArray[1024];
特点
  • 内核内部声明
  • 使用 __shared__ 说明符
  • 大小必须在编译时确定
  • 所有块内线程都可访问
示例
cpp 复制代码
__global__ void kernel() {
    __shared__ int cache[256];
    
    cache[threadIdx.x] = threadIdx.x;
    __syncthreads();
    
    // 使用cache...
}

2.2.3.2.2. Dynamic Allocation (动态分配)

启动时指定大小
cpp 复制代码
// 第三个参数指定共享内存大小(字节)
kernel<<<grid, block, sharedMemoryBytes>>>();
内核内声明
cpp 复制代码
extern __shared__ float sharedArray[];
多个动态数组的手动分区

正确方法(注意对齐):

cpp 复制代码
extern __shared__ float array[];

short* array0 = (short*)array;                    // 2字节对齐
float* array1 = (float*)&array0[128];              // 4字节对齐
int*   array2 =   (int*)&array1[64];                // 4字节对齐

错误示例(未对齐):

cpp 复制代码
extern __shared__ float array[];
short* array0 = (short*)array;
float* array1 = (float*)&array0[127];  // ❌ 未4字节对齐

对齐要求

  • 指针必须按指向类型对齐
  • short需要2字节对齐,float/int需要4字节对齐

2.2.3.3. Registers (寄存器)

基本概念

  • 物理位置:SM内部
  • 作用域:线程局部
  • 管理方式:由编译器自动管理

关键特性

特性 说明
速度 最快的存储类型
容量 有限,依GPU架构而异
分配 编译器为每个线程分配
查询 regsPerMultiprocessorregsPerBlock

寄存器查询

cpp 复制代码
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device);
printf("Registers per SM: %d\n", prop.regsPerMultiprocessor);
printf("Registers per block: %d\n", prop.regsPerBlock);

控制寄存器使用:-maxrregcount

bash 复制代码
nvcc -maxrregcount=32 kernel.cu  # 限制内核最多使用32个寄存器/线程
影响
  • 减少寄存器使用 → 可在SM上调度更多线程块
  • 过度减少 → 导致寄存器溢出(spilling),变量被移至局部内存
  • 权衡:并行度 vs 每个线程的性能

2.2.3.4. Local Memory (局部内存)

基本概念

  • 逻辑作用域:线程局部
  • 物理位置全局内存
  • 命名来源:名称来自逻辑作用域,而非物理位置

使用场景

编译器将自动变量放入局部内存的情况:

场景 说明
索引不确定的数组 数组索引不是编译时常量
大型结构或数组 占用寄存器空间过大
寄存器溢出 内核使用寄存器超过可用数量

性能特性

特性 说明
延迟 与全局内存相同(高延迟)
带宽 与全局内存相同
合并访问 连续线程访问连续32位字时可合并

局部内存的合并访问

局部内存组织方式

  • 连续32位字被连续线程ID访问
  • 只要warp内所有线程访问相同相对地址 ,访问就是完全合并

示例

cpp 复制代码
// 如果arr被编译器放入局部内存
int arr[4];
arr[0] = threadIdx.x;  // 所有线程访问arr[0] → 合并访问
arr[1] = data[threadIdx.x];  // 不同索引 → 可能无法合并

内存类型对比总结

特性 寄存器 局部内存 共享内存 全局内存 常量内存
位置 SM内部 设备内存 SM内部 设备内存 设备内存
作用域 线程 线程 网格 网格
生命周期 内核 内核 内核 应用程序 应用程序
速度 最快 快(缓存时)
管理 编译器 编译器 程序员 程序员 程序员
容量 很小 很大 小(但缓存)

最佳实践总结

  1. 全局内存

    • 主要数据存储,注意数据竞争
    • 合并访问以提高带宽利用率
  2. 共享内存

    • 用于块内线程协作和频繁访问的数据
    • 使用__syncthreads()确保数据一致性
    • 动态分配时注意对齐要求
  3. 寄存器

    • 编译器自动管理,通常最优
    • 必要时通过-maxrregcount限制使用量
    • 避免过度限制导致寄存器溢出
  4. 局部内存

    • 尽量使数组索引为编译时常量
    • 注意合并访问模式
  5. 内存选择策略

    • 频繁访问的数据 → 共享内存
    • 只读数据 → 常量内存
    • 线程私有数据 → 寄存器
    • 大容量、跨线程共享数据 → 全局内存

核心API总结表

API 用途
cudaMalloc() 分配全局内存
cudaFree() 释放全局内存
cudaMemcpy() 主机-设备数据传输
cudaGetDeviceProperties() 获取设备属性(内存大小等)
cudaFuncSetCacheConfig() 设置缓存/共享内存偏好
__syncthreads() 块内线程同步

相关推荐
有个人神神叨叨3 小时前
AI Coding 时代的企业级应用架构
人工智能·架构
星爷AG I4 小时前
14-2 个体、任务与环境(AGI基础理论)
人工智能·agi
飞Link4 小时前
深度解析 LSTM 神经网络架构与实战指南
人工智能·深度学习·神经网络·lstm
前端不太难4 小时前
AI 时代,鸿蒙 App 还需要传统导航结构吗?
人工智能·状态模式·harmonyos
格林威4 小时前
工业相机图像高速存储(C#版):内存映射文件方法,附Basler相机C#实战代码!
开发语言·人工智能·数码相机·c#·机器视觉·工业相机·堡盟相机
geneculture4 小时前
AGI Maths融智学AGI数学模型
人工智能·融智学的重要应用·哲学与科学统一性·信息融智学·融智时代(杂志)·agi maths.
OpenMMLab4 小时前
Agent范式转移:组织、协作与商业的重构
人工智能·大模型·多模态大模型·智能体·openclaw
love530love4 小时前
Windows 11 源码编译 vLLM 0.16 完全指南(RTX 3090 / CUDA 12.8 / PyTorch 2.7.1)
人工智能·pytorch·windows·python·深度学习·vllm·vs 2022