__ldg
功能:從 global memory 通過只讀緩存(read-only cache)加載數據。
template <typename T>
__device__ __forceinline__ T __ldg(const T* ptr);
注意這並不是函數原型,但是你可以這麼理解,T並不是所有類型都能適配的。
- 在老架構(Maxwell/Kepler)上,它會使用 LDG 指令,利用只讀緩存(texture cache / L1 read-only)。
- 在現代架構(Pascal+)仍然有效,但新架構 L2 的自動緩存更智能。
- 應用場景:當你的 global memory 數據只讀且多線程共享,使用
__ldg可以減少對 L1 的壓力,提高帶寬利用。
__global__ void kernel(const float* data, float* out) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
float val = __ldg(&data[idx]);
out[idx] = val * 2.0f;
}
|
架構代號
|
Compute Capability
|
Read-Only Cache 狀態
|
|
|
Kepler (GK110) |
3.5
|
✅ 獨立的只讀緩存
|
有效,大幅優化
|
|
Maxwell / Pascal |
5.x / 6.x
|
✅ 仍保留,只讀緩存共享與 L1
|
有效,但作用減弱
|
|
Volta / Turing / Ampere |
7.x / 8.x
|
⚠️ 與 L1 Cache 合併
|
|
|
Hopper (H100) |
9.0
|
❌ 不再單獨實現
|
|
在 Ampere 及以後:
- Read-only cache 不再是獨立單元;
- 所有 load 都經過一個統一的 L1 Cache(Unified Data Cache);
__ldg()仍然存在,編譯器不會報錯;- 但 PTX 層會退化為普通的
LDG.E或LDG.G指令,與*ptr無區別。
__prefetch_global
功能:提前將 global memory 數據加載到 cache,隱藏訪問延遲。
template <typename T>
__device__ void __prefetch_global(const T* ptr);
説明:
- Ampere 架構開始支持。
- 只是 hint,不保證立即訪問到數據,只是告訴硬件“接下來可能用到”。
- 對延遲敏感的循環訪問場景非常有用。
__global__ void kernel(float* data) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
__prefetch_global(&data[idx]);
float val = data[idx]; // 實際訪問時可能已經在 L1/L2
}
但是普通的遊戲顯卡,比如3090,4090可能不支持
|
架構
|
GPU類型
|
SM版本
|
是否支持 |
説明
|
|
A100 |
數據中心
|
sm_80
|
✅ 支持
|
pipeline copy / prefetch 全支持
|
|
H100 |
數據中心
|
sm_90
|
✅ 支持
|
支持更多 pipeline 指令
|
|
RTX 3090 / 3080 |
消費級
|
sm_86
|
❌ 不支持
|
指令未開放,prefetch 會報錯
|
|
RTX 4090 (Ada) |
消費級
|
sm_89
|
❌ 不支持
|
同樣未公開 prefetch API
|
__ldcs() / __ldcg() / __ldca()
功能:控制加載時的緩存策略。
|
API
|
含義
|
|
|
load via streaming cache(只 L2)
|
|
|
load global memory (cached)
|
|
|
load via L1 cache
|
template <typename T>
__device__ T __ldcs(const T* ptr);
template <typename T>
__device__ T __ldcg(const T* ptr);
template <typename T>
__device__ T __ldca(const T* ptr);
説明:
- Ampere/Hopper 架構提供,允許程序員控制 L1/L2 的命中策略。
- 對優化多線程數據共享、減少 L1 衝突有幫助。
- 對大多數通用算法不是必需,更多是高性能調優手段。
__stcs() / __stcg() / __stwb()
功能:控制 store(寫)時緩存策略。
|
API
|
含義
|
|
|
streaming store (L2 only)
|
|
|
cached store
|
|
|
write-back store
|
説明:
- 可以降低 L1 負擔或者控制寫合併策略。
- 對性能優化高併發寫場景有時有效。
__pipeline_memcpy_async
功能:異步拷貝數據到 shared memory / register / L2 pipeline。
- 原型(示意):
template <typename T>
__device__ void __pipeline_memcpy_async(T* dst, const T* src, size_t n);
説明:
- Ampere+ GPU 支持。
- 類似
cp.async指令,但 API 級別,更安全。 - 允許 overlap memory copy 與 compute,隱藏 global memory 訪問延遲。
- 必須配合
__pipeline_commit()和__pipeline_wait_prior()控制階段。
__pipeline_commit() / __pipeline_wait_prior()
功能:控制 pipeline 的異步拷貝階段。
- 用法:
__pipeline_commit():提交之前的 async copy 指令到 pipeline。__pipeline_wait_prior():等待 pipeline 中之前提交的 copy 完成。
- 説明:
- 通常配合循環或雙緩衝使用,實現 compute 與 copy 的 overlap。
- 相當於 Ampere+ 的“軟件可控 cp.async”。
for(int i=0;i<N;i+=tile){
__pipeline_memcpy_async(sm_tile, &gmem[i], tile);
__pipeline_commit();
__pipeline_wait_prior(); // 等待上一次拷貝完成
compute(sm_tile);
}
GPU緩存結構
CUDA GPU 的存儲層級(從慢到快)大致是:
Global Memory (DRAM)
│
▼
L2 Cache ← 所有 SM 共享
│
┌──────┴──────────┐
│ │
▼ ▼
L1 Data Cache Read-Only Cache (texture)
│
▼
Registers / Shared Memory
- Global Memory (DRAM):顯存,全局可訪問,但延遲高(幾百個 cycles)。
- L2 Cache:芯片級共享緩存,所有 SM 都能訪問,延遲較低(幾十個 cycles)。
- L1 Cache:每個 SM 獨立的一級緩存,延遲更低(10-20 cycles)。
- Read-Only Cache:專門優化只讀訪問(只在老架構中單獨存在,現在常與 L1 合併或共享)。
- Registers / Shared Memory:線程或線程塊級的高速存儲,延遲極低。
我們來對照看下不同指令的緩存行為。
|
指令 / API
|
訪問路徑
|
緩存層級
|
特點
|
典型用途
|
|
|
Global → Read-only cache → Register
|
L2 + Read-only(L1T) |
只讀緩存,不污染普通 L1
|
共享常量、查表數據
|
|
|
Global → L2 → L1 → Reg
|
L2 + L1 |
默認路徑(全緩存)
|
一般數據加載
|
|
|
Global → L1 only → Reg
|
L1 only(不走 L2) |
強制優先用 L1,L2可不命中
|
臨時數據,多次局部訪問
|
|
|
Global → L2 only → Reg
|
L2 only(跳過 L1) |
不污染 L1 cache
|
大流量讀取(streaming read)
|
但實際上,這些全是“建議式控制”,不是顯式管理。
L1/L2/只讀緩存的對比總結
|
緩存層
|
作用範圍
|
容量
|
一致性
|
可寫
|
典型用途
|
|
L1 Data Cache |
每個 SM 獨立
|
小(128KB~192KB)
|
不全局一致
|
可寫
|
局部數據緩存
|
|
Read-only Cache |
每個 SM 獨立
|
小(48KB~128KB)
|
無需一致性
|
只讀
|
常量、查表
|
|
L2 Cache |
全 SM 共享
|
大(幾 MB)
|
全局一致
|
可寫
|
跨 SM 通信、共享數據
|
實際調優經驗
|
場景
|
推薦策略
|
理由
|
|
常量查表 / 只讀共享
|
|
走 read-only cache,不污染 L1
|
|
大數據流式讀
|
|
避免 L1 被大量數據污染
|
|
局部複用強(tile/block 內)
|
|
強制走 L1,本地複用效率高
|
|
一般數據訪問
|
默認 / |
默認行為即可
|
|
Ampere+ 異步拷貝
|
|
取代手動 prefetch,性能更高
|
SM 與緩存的關係
SM(Streaming Multiprocessor) 是 GPU 的計算核心單元,就像 CPU 裏的“核(core)”。
一張 GPU(比如 A100、RTX 4090)內部通常有 幾十到上百個 SM。
CUDA 的層級關係:
Grid → Block → Warp → Thread
而 SM 是執行的硬件實體:
|
概念
|
含義
|
對應的硬件
|
|
Grid
|
一次 kernel 啓動的所有線程集合
|
GPU 整體
|
|
Block
|
一組線程(可通信/同步)
|
分配給一個 SM 執行 |
|
Warp
|
32 個線程組成的調度單元
|
SM 內的調度器發射
|
|
Thread
|
單個執行流
|
SM 內的計算核心(ALU)
|
每個 block 在運行時都會被分配到某個 SM 上執行,
block 內的所有線程都在那個 SM 的共享資源(如 shared memory、L1 cache)中運行。
每個 SM 有自己獨立的 L1 Cache 和 Shared Memory,所以:
- 一個 SM 裏的線程塊(block)可以在 shared memory 中高速共享數據;
- 不同 SM 之間的數據共享必須通過 L2 Cache 或 Global Memory;
- L1 cache 不同步(每個 SM 自己的 L1 可能不一致);
- L2 cache 在所有 SM 間是 一致的(coherent)。
這也就是為什麼有下面這種訪問策略:
|
緩存類型
|
是否 SM 共享
|
一致性
|
示例 API
|
|
L1 Cache |
❌ 獨立
|
不一致
|
|
|
Read-only Cache |
❌ 獨立
|
不一致
|
|
|
L2 Cache |
✅ 所有 SM 共享
|
一致
|
|
|
Global Memory |
✅ 所有 SM 共享
|
一致
|
—
|
SM的小知識
可以用下面的代碼查看自己 GPU 的 SM 數量:
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
std::cout << "SM數量: " << prop.multiProcessorCount << std::endl;
比如3090輸出82
“核函數一次最多啓動的 block 數不能超過 SM 數”
這是 不對的。
block 數可以遠遠多於 SM 數,只不過 同時在 GPU 上運行的 block 數受到 SM 數量和資源限制。
正確理解
核函數啓動時你寫的:
myKernel<<<numBlocks, threadsPerBlock>>>(...);
裏指定的 numBlocks 是整個 Grid 的大小。
GPU 會把這些 block 分批調度到 SM 上執行。
舉個具體例子(比如 A100):
- 每個 SM 最多同時運行 2048 線程
- 每個 block 有 256 線程
- 那麼每個 SM 同時最多能運行 8 個 block
- 若總共有 108 個 SM,則同時最多有
108 × 8 = 864個 block 在執行
但如果你啓動了 10,000 個 block,其他的會排隊等前面的執行完再上。
|
層次
|
存儲位置
|
是否共享
|
説明
|
|
寄存器 (Registers) |
每個線程私有
|
❌
|
每個線程自己的最快速存儲空間(延遲 < 10 cycles)。編譯器自動分配。
|
|
共享內存 (Shared Memory) |
每個 Block 獨佔
|
✅(同 Block 內共享)
|
位於 SM 內部的片上 SRAM。延遲 ~100 cycles,比 global memory 快很多。
|
|
L1 Cache |
每個 SM 獨佔
|
✅(同 SM 上的所有 Block 共享)
|
用於緩存 global memory 加載的結果,可配置與 Shared Memory 共用物理空間。
|
|
L2 Cache |
GPU 所有 SM 共享
|
✅(全局共享)
|
位於芯片片上(on-chip),負責不同 SM 之間的數據共享與一致性。
|
|
Global Memory (DRAM) |
芯片外顯存
|
✅(全局共享)
|
訪問延遲幾百到上千 cycles。
|