GPU Convolutional Layer:CUDA Kernel 與 GEMM 化
重點總覽 (Overview)
| 主題 | 直接 CUDA Kernel (§16.3) | GEMM 化 / im2col (§16.4) |
|---|---|---|
| 核心想法 | 每個 thread 算一個 output pixel,平行展開 N·M·H_out·W_out |
把 conv 攤平成一次大型矩陣乘法,交給 cuBLAS GEMM |
| 平行度來源 | 4 層「easy」迴圈 n, m, h, w |
矩陣乘法本身的大規模平行 |
| Thread organization | 2D block TILE_WIDTH×TILE_WIDTH;3D grid (M, T, N) |
1D block;unroll_Kernel 共 C·H_out·W_out threads |
| 主要瓶頸 | global memory bandwidth-bound | unroll 寫+讀放大記憶體流量,降低 arithmetic intensity |
| 關鍵指標 | parallel iterations = N·M·H_out·W_out |
expansion ratio → K·K |
| 優化方向 | constant memory + shared-memory tiling(同 Ch.7) | 用 1 個 buffer 逐 sample 重用;cuDNN 改為 on-chip lazy materialization |
兩種做法殊途同歸:CNN 訓練/推論的 conv layer 既 compute-intensive 又高度平行,計算型態本質上「像矩陣乘法」。§16.3 直接寫 kernel,§16.4 索性真的轉成 GEMM 借力高度優化的函式庫。
卷積層的可平行性 (Parallelism in the Convolutional Layer)
回顧 16-Deep-Learning/02-Convolutional-Neural-Networks-Layers 的 minibatch forward 程式碼 (Fig. 16.12) 共有 7 層迴圈:
for n // minibatch sample ┐
for m // output feature map │ 4 層 "easy" parallel
for h // output row │ iterations = N·M·H_out·W_out
for w // output col ┘
for c // input channel ┐ 3 層需 atomic 累加
for p // filter row │ (read-modify-write 同一個 Y)
for q // filter col ┘ → 預設保持序列
| 迴圈層 | 是否平行 | 原因 |
|---|---|---|
n, m, h, w |
easy parallel | 各 iteration 寫不同 Y[n,m,h,w],互不干擾 |
c, p, q |
需 atomic | 多 iteration 累加進同一個 Y element(race condition) |
- 公式:可用平行 iteration 總數 =
N · M · H_out · W_out,其中H_out = H - K + 1、W_out = W - K + 1。 - 這四層已足夠餵滿 GPU,故
c/p/q留在 thread 內序列執行,避免 atomic 的開銷。
若 M、H_out、W_out 都很小(例如網路末端 feature map 已縮到 1×1),easy parallelism 不足時,才考慮把 c/p/q 也平行化並付出 atomic 成本。
直接 CUDA 推論 Kernel (Direct CUDA Inference Kernel)
Thread → data 映射設計
- 每個 thread 算 1 個 output pixel。
- 2D thread block:每個 block 負責一個 output feature map 上
TILE_WIDTH × TILE_WIDTH的 tile(TILE_WIDTH=16→ 256 threads/block)。 - 3D grid
(M, T, N):- X 維 =
M(output feature map 索引)。 - Z 維 =
N(minibatch sample 索引)。 - Y 維 =
T = H_grid · W_grid:把垂直/水平 tile 索引線性化塞進單一維度(因 X、Z 已被佔用)。
- X 維 =
grid (M, T, N),block (TILE_WIDTH, TILE_WIDTH, 1)
blockIdx.x = m ──► 哪一張 output feature map
blockIdx.z = n ──► 哪一個 minibatch sample
blockIdx.y ──► row-major 線性化的 tile 編號
一張 output feature map (W_grid=2, H_grid=2):
┌─────────┬─────────┐
│ tile0,0 │ tile0,1 │ blockIdx.y = 0,1,2,3
│ (by=0) │ (by=1) │ (row-major)
├─────────┼─────────┤
│ tile1,0 │ tile1,1 │ tile 內由 threadIdx.x/y
│ (by=2) │ (by=3) │ 定位 16×16 pixel
└─────────┴─────────┘
還原 tile 座標:
vertical_tile = blockIdx.y / W_grid
horizontal_tile = blockIdx.y % W_grid
Host code (Fig. 16.13)
#define TILE_WIDTH 16
W_grid = W_out / TILE_WIDTH; // horizontal tiles per output map
H_grid = H_out / TILE_WIDTH; // vertical tiles per output map
int T = H_grid * W_grid; // tiles per map, linearized into grid.y
dim3 blockDim(TILE_WIDTH, TILE_WIDTH, 1);
dim3 gridDim(M, T, N);
ConvLayerForward_Kernel<<<gridDim, blockDim>>>(/* C, W_grid, K, X, W, Y */);
Kernel (Fig. 16.15)
__global__ void
ConvLayerForward_Kernel(int C, int W_grid, int K,
float* X, float* W, float* Y) {
int m = blockIdx.x; // output feature map
int h = (blockIdx.y / W_grid) * TILE_WIDTH + threadIdx.y; // output row
int w = (blockIdx.y % W_grid) * TILE_WIDTH + threadIdx.x; // output col
int n = blockIdx.z; // minibatch sample
float acc = 0.0f;
for (int c = 0; c < C; c++) // sum over all input channels
for (int p = 0; p < K; p++) // KxK filter
for (int q = 0; q < K; q++)
acc += X[n, c, h + p, w + q] * W[m, c, p, q]; // row-major linearize!
Y[n, m, h, w] = acc;
}
程式中 X[n,c,h+p,w+q]、W[m,c,p,q]、Y[n,m,h,w] 是多維索引偽碼,實作時須依 row-major 手動線性化(見 03-Multidimensional-Grids-And-Data/02-Mapping-Threads-to-Multidimensional-Data)。同理 W_out/TILE_WIDTH 假設可整除,否則需 boundary check。
效能定位
此 kernel 平行度高但 global memory bandwidth-bound——和 基本 convolution kernel 完全相同的問題:每次 MAC 都從 global memory 重新讀 X 與 W。
解法(書中留作習題,原理見 Ch.7):
- Constant memory 快取 filter bank
W(小且全 block 共用)。 - Shared-memory tiling 重用 input patch(相鄰 output pixel 的 patch 大量重疊)。
GEMM 化:im2col Unrolling (Formulating Convolution as GEMM)
中心思想(Chellapilla et al., 2006):展開並複製 input pixel,讓計算某一 output pixel 所需的所有輸入剛好排成 unrolled 矩陣的一整欄 (column),於是整個 forward 變成一次大矩陣乘法,丟給 cuBLAS GEMM。
三個矩陣的形狀
| 矩陣 | 維度 | 是否複製資料 | 說明 |
|---|---|---|---|
Filter matrix W |
M × (C·K·K) |
無複製 | 每列 = 一張 output map 所需全部權重,僅是 filter bank 的 row-major 串接 |
Unrolled input X_unroll |
(C·K·K) × (H_out·W_out) |
有複製 | 每欄 = 算一個 output pixel 所需的全部輸入 |
Output Y |
M × (H_out·W_out) |
— | 每列 = 一張完整 output feature map(已 row-major,可直接餵下一層) |
- 高度
C·K·K:每個 output element 來自C張 input map、各取K·K個元素。 - 寬度
H_out·W_out:output feature map 的像素數。 - 注意
M不影響X_unroll——所有 output map 共用同一份展開輸入。
範例 (Fig. 16.16): C=3, 每張 3×3;M=2,每張 output 2×2;filter 2×2
filter matrix × X_unroll = Y
(2 × 12) (12 × 8...實為 4) (2 × 4)
┌────────────┐ ┌──────────────┐ ┌──────────┐
│ map0 weights│ │col=各 output │ │ out map0 │
│ map1 weights│ │ pixel 的 │ → │ out map1 │
└────────────┘ │ 輸入 patch │ └──────────┘
└──────────────┘
一欄展開示意 (output (0,0)):
X 三張 map 的 patch 串接 → (1,2,1,1, 0,2,0,3, 1,2,0,1)ᵀ
filter 串接 → (1,1,2,2, 1,1,1,1, 0,1,1,0)
內積 = 14 = Y[0,0,0]
Expansion ratio(膨脹比)
由於 convolution 的 patch 彼此重疊,input pixel 被重複多次:
3×3 input map 中:
4 個角 pixel → 各用 1 次 (4·1)
4 個邊 pixel → 各用 2 次 (2·4)
1 個中心pixel → 用 4 次 (4·1)
展開後 = 4 + 8 + 4 = 16 個,原本 9 個 → ratio = 16/9 ≈ 1.8
- 一般公式:
expansion_ratio = (C·K·K·H_out·W_out) / (C·H_in·W_in)- 當 feature map 遠大於 filter 時 → 趨近
K·K。
- 範例:
(3·2·2·2·2)/(3·3·3) = 16/9。
Unroll CUDA kernel (Fig. 16.18)
設計:每個 thread 從一張 input map 蒐集 K·K 個元素,填入 unrolled 矩陣某欄的一段;總 thread 數 = C · H_out · W_out,用 1D block。
__global__ void
unroll_Kernel(int C, int H, int W, int K, float* X, float* X_unroll) {
int t = blockIdx.x * blockDim.x + threadIdx.x;
int H_out = H - K + 1, W_out = W - K + 1;
int W_unroll = H_out * W_out; // width of unrolled matrix
if (t < C * W_unroll) {
int c = t / W_unroll; // which input channel
int w_unroll= t % W_unroll; // column = linearized output pixel
int h_out = w_unroll / W_out;
int w_out = w_unroll % W_out;
int w_base = c * K * K; // section start row for channel c
for (int p = 0; p < K; p++)
for (int q = 0; q < K; q++) {
int h_unroll = w_base + p*K + q; // row in unrolled matrix
X_unroll[h_unroll, w_unroll] = X[c, h_out + p, w_out + q];
}
}
}
相較序列版 (Fig. 16.17),CUDA 版把原本最內層的 h/w 迴圈外提成 thread 平行維度(loop transformation)。結果:相鄰 thread(相鄰 w_unroll)寫入 X_unroll 同一列的相鄰位置 → coalesced write(見 06-Performance-Considerations/01-Memory-Coalescing)。
執行流程與記憶體取捨
- 資料佈局:input
N×C×H×W、filter 預先排成M×C×K²、outputM×(H_out·W_out)。 - 記憶體足跡控制:不一次展開整個 minibatch,只配置 1 個
X_unroll[C·K·K, H_out·W_out]buffer,逐 sample 重用 → 每個 sample 先unroll_Kernel、再 GEMM。
| 優點 (Pros) | 缺點 (Cons) |
|---|---|
| 借力高度優化的 cuBLAS GEMM | 複製輸入最多 K·K 倍,記憶體可能爆量 |
| GEMM 在 GPU 上 FLOP/byte 高,矩陣越大越有效率 | X_unroll 須寫一次又讀一次(外加讀 X),記憶體流量遠多於直接法 → 降低 arithmetic intensity |
各層矩陣維度多為乘積 (C·H_out·W_out 等),通常都很大 → 效能穩定高 |
逐 sample materialize 限制平行度,小矩陣難餵滿 GPU |
早期層 C 小但 H_out·W_out 大;末端層 C 大但 H_out·W_out 小——乘積 C·H_out·W_out 對所有層都偏大,故矩陣尺寸一致地大,GEMM 效能因而穩定。
缺點的最終解:cuDNN 不把 X_unroll 落到 off-chip global memory,而是 lazy 地在 on-chip memory 邊算 GEMM tile 邊生成展開資料,省下膨脹後的頻寬。
考試/面試重點 (Exam / Test Patterns)
| 情境 / 關鍵字 | 答案 / 技巧 |
|---|---|
| conv layer 有幾層 easy parallelism? | 4 層:n, m, h, w;總平行 iteration = N·M·H_out·W_out |
為何 c/p/q 不平行化? |
多 iteration 累加進同一個 Y element → 需 atomic(read-modify-write race),預設保持序列 |
| 直接 kernel 的 grid 維度安排? | (M, T, N):X=feature map、Z=sample、Y=H_grid·W_grid 線性化 tile 索引 |
從 blockIdx.y 還原 tile 位置? |
vert = blockIdx.y/W_grid、horiz = blockIdx.y%W_grid,再 ×TILE_WIDTH + threadIdx |
| 直接 kernel 的效能瓶頸? | global memory bandwidth-bound;解法 constant memory 放 filter + shared-memory tiling |
| im2col 的展開比是多少? | (C·K·K·H_out·W_out)/(C·H_in·W_in),feature map 大時 → K·K;範例 16/9 |
X_unroll 矩陣維度? |
(C·K·K) × (H_out·W_out);高度=每 output 所需輸入數,寬度=output pixel 數 |
| filter matrix 維度與是否複製? | M × (C·K·K),無複製,僅 row-major 串接 filter banks |
| GEMM 法的兩大缺點? | (1) 複製放大記憶體 (2) X_unroll 寫+讀降低 computational intensity |
| unroll kernel 為何能 coalesce? | loop interchange 讓相鄰 thread 寫 unrolled 矩陣同列相鄰位置 |
| cuDNN 如何避開 GEMM 法的缺點? | lazy materialization:在 on-chip memory 邊算 tile 邊生成 X_unroll,不落 off-chip |
Related Notes
- 16-Deep-Learning/02-Convolutional-Neural-Networks-Layers
- 16-Deep-Learning/04-cuDNN-Library-and-Summary
- 07-Convolution/03-Tiled-Convolution-and-Halo-Handling
- 03-Multidimensional-Grids-And-Data/04-Matrix-Multiplication-Kernel
- 05-Memory-Architecture-And-Data-Locality/02-Tiling-and-Tiled-Matrix-Multiplication
- 06-Performance-Considerations/01-Memory-Coalescing