SIMT 裝置記憶體空間 (SIMT Device Memory Spaces)

重點總覽

CUDA device 有多種記憶體空間,kernel 內的 thread 可依其 scope(範圍)lifetime(生命週期)physical location(實體位置) 存取。下表為原文 Table 1 的核心摘要,後續每個概念各有一節說明。

項目 重點
Global memory Grid scope、Application lifetime、實體在 Device;主要資料儲存、所有 thread 可見、persistent
Constant memory Grid scope、Application lifetime、實體在 Device;read-only、每 device 一份、典型 64KB
Shared memory Block scope、Kernel lifetime、實體在 SM;user-managed scratchpad、與 L1 共用實體空間
Local memory Thread scope、Kernel lifetime、實體在 Device(global space);register spilling 的去處
Register Thread scope、Kernel lifetime、實體在 SM;最快、由 compiler 管理、thread local
static __shared__ 編譯期決定大小、宣告在 kernel body 內
dynamic extern __shared__ launch 時用第三個 <<<>>> 參數決定大小、一個 kernel 僅能有一個
Caches (L2/L1) L2 全 SM 共用、L1 在每個 SM 且與 shared memory 共用實體空間
Texture/Surface 現代 GPU 對 non-texture load 已無效能優勢、僅渲染情境有用
Distributed Shared Memory CC 9.0+、thread block cluster 內跨 block 存取彼此 shared memory
Scope / Lifetime / Location 三維度

記住每種記憶體要同時掌握三件事:誰能看到它(scope)、它存活多久(lifetime)、實體在哪(location)。考試最愛混淆 Local memory:它的 scope 是 thread,但實體位置在 device(global)space,不在 SM。

存取速度(快 → 慢)           實體位置
Register ──────────────────  SM 晶片上
Shared / L1 ───────────────  SM 晶片上
L2 cache ──────────────────  Device(全 SM 共用)
Constant(透過 cache)─────  Device
Local / Global ────────────  Device(off-chip DRAM)

2.3.3.1 Global Memory

Global memory(又稱 device memory)是儲存 kernel 內所有 thread 皆可存取資料的主要空間,類似 CPU 系統中的 RAM。GPU 上的 kernel 直接存取 global memory,如同 CPU 程式存取系統記憶體。

__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 三個陣列都在 global memory,這個 vector add kernel 透過全域索引存取它們。

2.3.3.2 Shared Memory

Shared memory 是 thread block 內所有 thread 皆可存取的空間,實體位於每個 SM 上,與 L1 cache 共用同一塊實體資源(unified data cache)。

// 假設 blockDim.x 為 128
__global__ void example_syncthreads(int* input_data, int* output_data) {
    __shared__ int shared_data[128];
    shared_data[threadIdx.x] = input_data[blockDim.x * blockIdx.x + threadIdx.x];
    // 所有 thread 同步,保證對 shared_data 的寫入都排在解除阻擋之前
    __syncthreads();
    // 由單一 thread 安全讀取 shared_data
    if (threadIdx.x == 0) {
        float sum = 0;
        for (int i = 0; i < blockDim.x; ++i) sum += shared_data[i];
        output_data[blockIdx.x] = sum;
    }
}

寫入 shared memory 後,必須 __syncthreads() 才能讓其他 thread 安全讀取,否則為 data race。

L1 與 shared memory 共用實體空間

兩者搶同一塊 unified data cache。多用 shared memory,L1 可用量就變小;完全不用 shared memory 時整塊都歸 L1。

2.3.3.2.1 Static Allocation of Shared Memory

在 kernel body 內用 __shared__(C++)或 cuda.shared.array()(Python)宣告變數,大小必須在編譯期決定,存活整個 kernel 執行期間。

__shared__ float sharedArray[1024];

宣告後,block 內所有 thread 都能存取這個 shared 陣列。

2.3.3.2.2 Dynamic Allocation of Shared Memory

C++ 在 launch 時把每 block 所需 shared memory 位元組數放在 triple chevron 的第三個(可選)參數functionName<<<grid, block, sharedMemoryBytes>>>();未指定時預設為 0。Python 用 cuda.core.launch() 搭配 LaunchConfigshmem_size 欄位。

extern __shared__ float sharedArray[];
只能有一個動態 shared 陣列

一個 kernel 僅能有單一動態配置的 shared 陣列。若需多個,必須配置一個夠大的單一陣列再手動切割(partition)。

extern __shared__ float array[];
short* array0 = (short*)array;
float* array1 = (float*)&array0[128];
int*   array2 = (int*)&array1[64];

切割時 pointer 必須對齊到所指型別:若改成 &array0[127]array1 未對齊 4 bytes 就會出錯。Python 無 pointer,故無此 type punning。

2.3.3.3 Registers

Register 位於 SM 上,具 thread local scope,是 kernel 執行期間最快的 thread 私有儲存。

register 數與 occupancy 的取捨

少用 register 可塞更多 block 進 SM;但壓太兇會 spill 到 global memory,反而拖慢速度。這是 occupancy 調校的核心 trade-off。

2.3.3.4 Local Memory

Local memory 是與 register 類似、由 NVCC 管理的 thread local 儲存,但實體位置在 global memory space。「local」指的是邏輯 scope,不是實體位置。

別被「local」誤導

Local memory 名為 local,實體卻在 off-chip 的 device memory,速度跟 global memory 一樣慢——它不是晶片上的快取。

2.3.3.5 Constant Memory

Constant memorygrid scope,在整個 application lifetime 可用;位於 device,對 kernel 唯讀(read-only)

// 在 .cu 檔
__constant__ float coeffs[4];
__global__ void compute(float* out) {
    int idx = threadIdx.x;
    out[idx] = coeffs[0] * idx + coeffs[1];
}
// 在 host 程式
float h_coeffs[4] = {1.0f, 2.0f, 3.0f, 4.0f};
cudaMemcpyToSymbol(coeffs, h_coeffs, sizeof(h_coeffs));
compute<<<1, 10>>>(device_out);

host 端用 cudaMemcpyToSymbol 把資料寫入 __constant__ 符號,kernel 端唯讀存取。

2.3.3.6 Caches

GPU 具多層 cache 結構,包含 L2L1

Cache 位置 共用範圍 查詢
L2 Device 所有 SM 共用 l2CacheSizecudaGetDeviceProperties
L1 每個 SM 與 shared memory 共用實體空間 (見 shared memory)

2.3.3.7 Texture and Surface Memory

GPU 可能有「從影像載入資料作為 3D 渲染 texture」的特化指令,CUDA 以 texture object APIsurface object API 暴露。

新程式碼不要用 texture 來加速一般載入

現代 GPU 直接 load/store 即可,texture/surface 對非渲染用途已無 performance benefit。

2.3.3.8 Distributed Shared Memory

前提

Distributed shared memory 需搭配 thread block cluster,使用 cooperative_groups API,目前僅 C++ 可用。Thread Block Clusters 於 compute capability 9.0 引入。

#include <cooperative_groups.h>
namespace cg = cooperative_groups;
__global__ void clusterHist_kernel(int* bins, /* ... */) {
    extern __shared__ int smem[];
    cg::cluster_group cluster = cg::this_cluster();
    unsigned int clusterBlockRank = cluster.block_rank();
    // ... 初始化 smem 為 0 ...
    cluster.sync();                       // 確保全 cluster 的 shared memory 都已歸零且 block 都已啟動
    // 計算目標 block rank 與 offset
    int* dst_smem = cluster.map_shared_rank(smem, dst_block_rank);
    atomicAdd(dst_smem + dst_offset, 1);  // 跨 block atomic 更新 histogram bin
    cluster.sync();                       // 確保所有 distributed 操作完成後才退出
}

cluster.map_shared_rank() 取得遠端 block 的 shared memory 指標,是 distributed shared memory 的關鍵 API。

cudaLaunchConfig_t config = {0};
config.gridDim = array_size / threads_per_block;
config.blockDim = threads_per_block;
config.dynamicSmemBytes = nbins_per_block * sizeof(int);   // 動態 shared memory 大小是 per block
cudaFuncSetAttribute(clusterHist_kernel,
    cudaFuncAttributeMaxDynamicSharedMemorySize, config.dynamicSmemBytes);
cudaLaunchAttribute attribute[1];
attribute[0].id = cudaLaunchAttributeClusterDimension;
attribute[0].val.clusterDim.x = cluster_size;              // y, z 設 1
config.numAttrs = 1; config.attrs = attribute;
cudaLaunchKernelEx(&config, clusterHist_kernel, /* args... */);

cudaLaunchKernelEx 搭配 cudaLaunchAttributeClusterDimension 在 runtime 動態決定 cluster 維度;注意 dynamic shared memory 大小仍以 per block 指定。

考試/測驗重點

情境/關鍵字 答案
Local memory 實體在哪? device(global)memory space,不是 SM;速度等同 global memory
「local」指的是什麼? 邏輯 scope(thread local),不是實體位置
Shared memory 與哪個 cache 共用空間? L1 cache(unified data cache,在每個 SM 上)
不用 shared memory 時 L1 如何? 整塊實體空間都給 L1 cache
一個 kernel 能有幾個 dynamic shared 陣列? 僅一個;多個需自行配置大陣列再手動 partition
dynamic shared memory 大小在哪指定? C++ <<<grid, block, bytes>>> 第三參數(預設 0);Python LaunchConfig.shmem_size
static shared memory 大小何時決定? 編譯期(compile time)
Constant memory 典型大小/可寫嗎? 64KB/device、對 kernel read-only
host 如何寫入 __constant__ cudaMemcpyToSymbol(另有 cudaMemcpyFromSymbol 等)
kernel 回傳值如何回傳 host? kernel 為 void只能寫入 global memory
-maxrregcount 的副作用 可能更多 block 同時排程,但也可能更多 register spilling
register spilling 是什麼? register 值被寫出至 global memory 再讀回,以騰出空間
Distributed shared memory 需求的 CC? compute capability 9.0,搭配 thread block cluster + cooperative groups(僅 C++)
distributed shared memory 總大小公式 cluster 內 block 數 × 每 block shared memory 大小
跨 block 取遠端 shared memory 指標的 API cluster.map_shared_rank(smem, rank)
Texture/surface memory 在現代 GPU 對一般 load? 無效能優勢;僅渲染(如 OptiX hit shader)有用
cudaFuncSetCacheConfig 保證生效嗎? ,只是 preference,runtime 可不採納
哪些記憶體實體在 SM 上? Register、Shared memory(L1)
哪些記憶體實體在 Device 上? Global、Constant、Local、L2
Global memory scope / lifetime? Grid / Application(persistent,到 free 或程式結束)