CUDA Dynamic Parallelism (動態平行) 練習題 (Practice - Dynamic Parallelism Fundamentals and Overview)
Related Concepts
- 21-CUDA-Dynamic-Parallelism/01-Dynamic-Parallelism-Fundamentals — Dynamic Parallelism 基礎與概觀
- 21-CUDA-Dynamic-Parallelism/02-Bezier-Curves-Example — 範例:Bezier Curves 與動態工作量
- 21-CUDA-Dynamic-Parallelism/03-Recursive-Quadtree-Example — 遞迴範例:Quadtree
- 21-CUDA-Dynamic-Parallelism/04-Execution-Considerations-and-Summary — 重要執行考量與總結
| 關鍵字 / 情境 | 答案 / 重點 |
|---|---|
| Dynamic Parallelism 定義 | kernel 內可 launch 另一個 kernel;device thread 自行產生新 grid(早期 CUDA 只有 host 能 launch) |
| parent / child grid | 發起者 = parent grid;被啟動者 = child grid;child launch 語意同一次獨立 kernel launch |
launch 語法 <<<Dg,Db,Ns,S>>> |
Dg=grid 維度、Db=block 維度、Ns=動態 shared mem bytes(預設0)、S=stream(預設 NULL) |
S (stream) 限制 |
必須在同一 block 內建立;scope 為 block-private |
| Fig.21.4 for-loop 兩問題 | (1) 錯失可平行的迴圈工作 (2) warp 內迭代數不一 → control divergence |
| loop → child grid 轉換 | 每 child thread 只做 1 次迭代 → load balance 佳、消除 divergence、抽取更多平行度 |
| Bezier thread vs block | parent 工作小 → 一條 thread 一組控制點(原版用一個 block);grid = ceil(N_LINES/BLOCK_DIM) |
device-side cudaMalloc |
kernel 內依實際 nVertices 精準配置;必由 device kernel cudaFree 釋放(freeVertexMem) |
| child grid 數 (Bezier) | = N_LINES(每條 parent thread 各 1 個),與 parent block 數無關 |
| 可傳給 child 的記憶體 | global / constant / texture 可;local / shared 不可(thread/block 私有) |
| 兩個記憶體一致視圖時刻 | (1) parent launch child 時 (2) parent 同步於 child 完成時 |
| launch 後 parent 寫入 | child 不保證看得到(只保證 launch 前的寫入) |
| pending launch pool | 預設 2048;超過落入 virtualized pool,慢 一個數量級 (10×+) |
| 調整 pool | host 端 cudaDeviceSetLimit(cudaLimitDevRuntimePendingLaunchCount, N);只在預期 > 2048 才調 |
| NULL stream 陷阱 | 同 block 內所有 launch(即使不同 thread)被序列化;要併發須 per-thread named stream |
| named stream 建立 | cudaStreamCreateWithFlags(&s, cudaStreamNonBlocking),每 thread 一個 |
| nesting depth | 硬體上限 24 層;遞迴 kernel launch 前須檢查 depth < max_depth |
| synchronization depth | 有 parent-child 同步時需存 parent state → 比 nesting depth 更嚴格的限制 |
| quadtree 映射 | 1 quadrant = 1 block;一次 launch 固定 <<<4,...>>>(branch degree 4) |
| quadtree 三步驟 | count(shared-mem atomic) → 4-element scan(求 offset) → reorder(scatter);同 radix sort 結構 |
| nested parallelism 高效條件 | 樹要矮且胖:node 多 thread/block (thick nodes) + branch degree 大;深而瘦因 24 層上限受限 |
| 小 child grid 代價 | thread 太少 → GPU 嚴重低度利用 (underutilization) |
Question 1 - Dynamic Parallelism 是什麼 [recall]
情境/題目:用一句話定義 CUDA Dynamic Parallelism (CDP),並說明它解除了早期 CUDA 的什麼限制?哪些演算法特徵最受益?
CDP 是 CUDA 程式模型的擴充,讓 一個 kernel 內部可以 launch 另一個 kernel,使在 device 上執行的 thread 能自行產生新的 grid。早期 CUDA 中 grid 只能由 host code launch。最受益的演算法特徵:recursion、irregular loop structures、time-space variation(時空變化)、adaptive grid refinement——這些不適合「平坦且單層」平行度的工作,過去得靠多次 host kernel call 或迴圈序列化,增加 host 負擔與 host-device 通訊。
Question 2 - Kernel-in-kernel Launch 語法 [recall]
情境/題目:device 端從 kernel 內呼叫 kernel 的語法為何?
<<<Dg, Db, Ns, S>>>四個參數各代表什麼、預設值為何?S有什麼特別限制?
語法與 host 端完全相同:kernel<<<Dg, Db, Ns, S>>>(args)。
Dg(dim3):grid 維度與大小(block 數)。Db(dim3):每個 block 的維度與大小(thread 數)。Ns(size_t):每 block 動態配置的 shared memory bytes,額外於靜態配置,預設 0。S(cudaStream_t):此呼叫關聯的 stream,預設 0 (NULL stream)。
S 限制:stream 必須在發起呼叫的同一個 thread block 內建立,其 scope 為 block-private。
Question 3 - Fig. 21.4 迴圈版 Kernel 的兩個問題 [recall]
情境/題目:Fig. 21.4 中每個 thread 先做前置運算、再 for-loop 走訪自己負責的一串資料元素。這種寫法有哪兩個主要問題?把迴圈改成 child grid 後分別如何被解決?
兩個問題:(1) 錯失平行度——若迴圈體本可平行,卻被寫成序列執行;(2) control divergence——同一 warp 內各 thread 的迴圈次數差異大時,warp 必須等最長的 thread,閒置浪費。
CDP 解法(Fig. 21.5,拆成 parent + child):迴圈迭代改由 child kernel threads 並行執行(抽取更多平行度);每個 child thread 只做一次迭代 → load balance 佳、消除 divergence。
Question 4 - Bezier 版本的三項關鍵改動 [recall]
情境/題目:Bezier 範例從無 CDP(Fig. 21.6)改成有 CDP(Fig. 21.7)時,做了哪三項關鍵改動:索引單位、記憶體配置、記憶體釋放?
(1) 索引單位:parent 用 一條 thread 處理一組控制點(索引以 thread 為基底 threadIdx.x+blockDim.x*blockIdx.x),原版用一個 block(索引以 blockIdx.x);因為 parent 工作(算 curvature + launch)很小。
(2) device-side cudaMalloc:vertexPos 改成指標,在 kernel 內依實際 nVertices 精準配置記憶體,避免原版每條線固定配 MAX_TESS_POINTS(最壞情況)造成的浪費。
(3) 釋放:device 端配的記憶體必須由 device kernel cudaFree 釋放,故另寫 freeVertexMem kernel 由 host 呼叫平行釋放(host 不能直接 free device 端 cudaMalloc 的記憶體)。
Question 5 - Parent/Child 記憶體可見性 [recall]
情境/題目:parent thread 可以把哪些記憶體類型的指標傳給 child grid?哪些不行?parent 與 child 「記憶體視圖一致」的兩個保證時刻是什麼?
可傳:global、constant、texture memory(皆 device 全域共享)。不可傳:local memory(thread 私有)、shared memory(block 私有)——傳了會讓 child 存取無效位址。
兩個一致視圖時刻:(1) parent launch child 那一刻——launch 之前的所有寫入 child 一定看得到,launch 之後的寫入無保證;(2) parent 同步於 child 完成那一刻——在此之前 child 的寫入不保證對 parent 可見。心法:要給 child 讀的就在 launch 前寫完;要讀 child 結果就先同步再讀。
Question 6 - Pending Launch Pool [recall]
情境/題目:什麼是 pending launch pool?預設容量多少?超過會怎樣?如何調整?什麼時候才該調?
Pending launch pool 是追蹤「執行中或等待執行」kernel 的固定大小緩衝區,預設可容納 2048 個 pending kernel calls。超過 → 啟用 virtualized pool,效能掉一個數量級(10× 以上)。
調整:在 host 端、launch parent 之前呼叫 cudaDeviceSetLimit(cudaLimitDevRuntimePendingLaunchCount, N),把固定 pool 設為「預期 launch 的 grid 數」。一般建議:只在預期 launch 數超過預設 2048 時才調高;調得比實際需求小(如 1024)既無加速也無意義。
Question 7 - Per-Thread Streams 與序列化 [recall]
情境/題目:device thread launch child grid 時若不指定 stream 會發生什麼?stream 的作用範圍是什麼?要讓同 block 內不同 thread 的 child grid 併發該怎麼做?
不指定 stream → 使用 default NULL stream,同一 block 內所有 launch(即使來自不同 thread)都被序列化。stream 的 scope 是 block-private(不可跨 block 使用別 block 建的 stream)。
要併發須讓每個 thread 建立自己的 non-blocking named stream:
cudaStream_t s;
cudaStreamCreateWithFlags(&s, cudaStreamNonBlocking);
child<<<gridDim, blockDim, 0, s>>>(...);
cudaStreamDestroy(s);
否則 Bezier 這類「同 block 多 thread 各 launch child」的程式平行度甚至會比沒用 CDP 還差。
Question 8 - Nesting Depth 與 Synchronization Depth [recall]
情境/題目:什麼是 nesting depth?硬體上限多少?遞迴 kernel(如 quadtree)為何 launch 前必須檢查它?synchronization depth 與它有何不同?
Nesting depth = child kernel 再 launch kernel、層層相疊所達到的總層數,目前硬體上限為 24 層。像 quadtree 這種遞迴 kernel launch 前必須先檢查(if (depth >= max_depth) return;),否則超過上限會 launch 失敗。
Synchronization depth:當存在 parent-child 同步時,系統需保存 parent grid 的狀態,記憶體成本更高,因此對巢狀深度施加額外、更嚴格的限制(≤ nesting depth)。
Question 9 - Constant 與 Shared/Local 的可見性 (Ex.4 & Ex.5) [recall]
情境/題目:判斷真偽並說明理由:(a) parent kernel 可以定義新的
__constant__變數讓 child kernel 繼承。(b) child kernel 可以存取其 parent 的 shared memory 與 local memory。
(a) False。__constant__ 是編譯期 file-scope 宣告,不是 runtime 動態建立的,parent 無法在執行期「定義新的」constant 變數給 child 繼承。
(b) False。shared memory 是 block 私有、local memory 是 thread 私有;parent 不可把它們的指標傳給 child grid,child 也無法存取(會是無效位址)。child 只能存取 global / constant / texture memory。
Question 10 - Bezier 計數題 (Ex.1) [application]
情境/題目:Bezier 範例中,判斷以下敘述真偽:(a)
N_LINES=1024, BLOCK_DIM=64→ launch 16 個 child kernel。(b)N_LINES=1024時應把 fixed pool 從 2048 調成 1024 以求最佳效能。(c)N_LINES=1024, BLOCK_DIM=64且用 per-thread streams → 共 16 個 stream。
(a) False。每條 parent thread 各 launch 一個 child grid → child kernel 數 = N_LINES = 1024,不是 16;16 只是 parent block 數 = N_LINES/BLOCK_DIM = 1024/64。
(b) False。預設 2048 已足夠涵蓋 1024 個 launch;調小不會變快,反而可能限制。只有「預期 launch 數 > 2048」才該調高。
(c) False。per-thread stream 是每條 thread 各建一個 → 共 1024 個 stream,不是 16。
Question 11 - Quadtree 深度與 Launch 數 (Ex.2 & Ex.3) [application]
情境/題目:64 個等距點以 quadtree 分類(每象限點數須 > 2 才再細分,完美均分)。(a) quadtree 的最大深度(含 root)是多少?(b) 總共啟動多少個 child kernel launch?
點數每深一層除以 4:64 → 16 → 4 → 1。
(a) 最大深度 = 4(depth 0, 1, 2, 3)。depth 0 有 64 點、depth 1 每節點 16 點、depth 2 每節點 4 點(>2 仍切)、depth 3 每節點 1 點(≤2 停)。
(b) depth 0、1、2 的所有 active 節點都會 launch(每次 launch 一個 <<<4>>> child grid),節點數為 1、4、16 → 總 child kernel launch = 1 + 4 + 16 = 21 = (4³−1)/3。(depth 3 各節點只剩 1 點,不再 launch。)
Question 12 - NULL Stream 下的併發上限 (Ex.6) [application]
情境/題目:6 個 block、每 block 256 threads 執行一個 parent kernel,每條 thread launch 一個 child kernel,且使用 default NULL stream。最多有幾個 child kernel 能併發執行?
6 個。stream scope 是 block-private,NULL stream 使同一 block 內所有 launch 序列化,所以每個 block 同時最多只有 1 個 child grid 在跑;6 個 block 彼此獨立可併發 → 最多 6 × 1 = 6 個 child kernel 併發。(若改用 per-thread named streams,則同 block 內的 child grid 才能併發,上限大幅提高。)
Question 13 - For-loop vs Child-grid:何時反而更糟 [analysis]
情境/題目:對「每個工作單位工作量動態變化且差異大」的問題,比較「無 CDP 的 for-loop 版」與「有 CDP 的 child-grid 版」在平行度、control divergence、SM 利用率上的差異。CDP 版在什麼情況下反而比原版更慢?
for-loop 版:迴圈被序列執行 → 錯失平行度;warp 內迭代數不一 → control divergence;block 間迭代數差異大 → 部分 SM 提早閒置、利用率下降。
child-grid 版:迴圈迭代由 child threads 並行(抽取更多平行度);每 child thread 只做 1 次迭代 → load balance 佳、消除 divergence、SM 利用率提升。
CDP 版反而更慢的情況:(1) child grid 太小(thread 太少)→ 嚴重 GPU underutilization,launch overhead 蓋過收益;(2) 沿用 default NULL stream → 同 block 的 child grid 全部序列化,平行度甚至比原版更差,須改用 per-thread named streams;(3) launch 數超過 pending pool(2048)落入 virtualized pool,慢一個數量級。結論:CDP 收益需「夠胖的 child grid + named streams + 足夠的 pool」才能兌現。
Question 14 - Quadtree Reorder:Scatter vs Gather [analysis]
情境/題目:quadtree 的
reorder_points()用 scatter(對象限計數器atomicAdd取得目的 index 再寫出)把點分群。為什麼這裡選 scatter 而非 gather?scatter 的代價是什麼?整個「count → scan → reorder」流程與哪個經典演算法同構?
為何 scatter:reorder 的目標是把任意分布的輸入點依所屬象限動態分群到輸出 buffer,每個象限的輸出長度執行期才知道、且要連續排列。gather 要求每個 thread「固定知道自己要讀哪個來源」,但這裡是輸入決定輸出位置(一對多分群),無法事先靜態映射 → 必須 scatter:對 smem2[q] 做 atomicAdd 動態取得該象限「下一個可寫位置」。
代價:需要 atomic(搶象限內下一格),有競爭成本;寫出位置不規則,較難 coalesce。但因計數器只有 4 個且在 shared memory,atomic 競爭遠比 global 便宜。
同構:count(shared-mem atomic)→ 4-element scan(求 offset)→ scatter(reorder) 與 radix sort 的 count → scan → scatter 三步完全同構。
Question 15 - 4-element Scan:序列 vs 平行 [analysis]
情境/題目:quadtree 的
scan_for_offsets()對 4 個象限計數器做 exclusive scan,卻只用單一 thread 序列完成,而非套用平行的 Kogge-Stone / Brent-Kung。請說明這個選擇的理由,並說明它呼應了什麼「演算法選擇」原則。
元素只有 4 個,序列 scan 僅需 3 次加法、無 barrier、無 SIMD 浪費;若改用平行 Kogge-Stone,反而要付出 __syncthreads()、額外 shared memory、warp 內多數 lane 閒置等 overhead,對這麼小的問題完全不划算。平行 scan 的 work(O(N log N))與同步成本只有在 N 很大時才回本。
這呼應「演算法選擇要看資料規模」的原則:最佳演算法依輸入大小而異——小資料用最簡單的序列法、大資料才用平行法。與 Kogge-Stone vs Brent-Kung 的取捨(work-efficiency vs 步數)同屬「沒有放諸四海皆準的最佳解」這一類。
| 主題 / 機制 | 關鍵規則 / 數字 | 出錯後果 / 取捨 |
|---|---|---|
| Dynamic Parallelism 定義 | kernel 內可 launch kernel;device thread 產生 child grid | 取代「終止→回報 host→重 launch」的往返 |
| launch 語法 | <<<Dg,Db,Ns,S>>>;Ns預設0、S預設NULL 且須同 block 建立 |
用錯 stream/省略 S → 序列化 |
| loop → child grid | 每 child thread 做 1 次迭代 → 抽取平行度、消 divergence、load balance | child grid 太小 → GPU underutilization |
| Bezier 三改動 | thread-per-line + device cudaMalloc + freeVertexMem(device free) |
child kernel 數 = N_LINES(非 block 數) |
| 記憶體可見性 | global/constant/texture 可傳;shared/local 不可 | 傳 shared/local → child 讀到無效位址 |
| 兩個一致視圖時刻 | (1) launch child 時 (2) parent 同步 child 完成時 | launch 後 parent 寫入 child 不保證可見 |
| pending launch pool | 預設 2048;cudaDeviceSetLimit(...PendingLaunchCount, N) |
超量 → virtualized pool,慢 10×+ |
| per-thread streams | NULL stream 同 block 序列化;named stream 才併發;scope block-private | 沿用 NULL → 平行度比沒用 CDP 還差 |
| nesting / sync depth | nesting 上限 24;有同步時 sync depth 更嚴格 | 遞迴 launch 前須查 depth,否則失敗 |
| quadtree 三步 | count(shared atomic) → 4-elem scan → reorder(scatter);同 radix sort | 1 quadrant=1 block;固定 <<<4>>> branch degree |
| nested parallelism 高效 | 樹要矮且胖:thick nodes + 大 branch degree | 深而瘦因 24 層上限與小 grid 無法高效 |
核心心法:Dynamic Parallelism 把「動態發現工作 → 立即就地平行化」的迴路留在 device 上,藉此抽取更多平行度、消除 warp 內 control divergence、並用 device-side cudaMalloc 精準配置記憶體。但它不是免費午餐——收益必須靠「夠胖的 child grid + per-thread named streams + 足夠的 pending pool + 受控的 nesting depth」才能兌現;否則小 grid 的 underutilization、NULL stream 的序列化、virtualized pool 的退化,都可能讓 CDP 版比原始版本更慢。