進階 Streams 與相依啟動 (Advanced Streams and Dependent Launch)
重點總覽
| 項目 | 重點 |
|---|---|
| Stream 序列化與並行 | 同一 stream 內預設序列化;唯一例外是 PDL。不同 stream 在無 event 相依、無 implicit sync、資源足夠時可並行 |
| NULL stream 阻斷 | 中間若對 NULL stream 下任何指令,會阻斷不同 stream 並行;non-blocking stream(cudaStreamNonBlocking)可避免 |
| 最小同步原則 | 選擇「剛好夠用」最不一般化的同步方式;偏好 stream/event 層級而非整個 device |
| Stream Priorities | cudaStreamCreateWithPriority() 設優先級;範圍由 cudaDeviceGetStreamPriorityRange() 取得;只是 hint 不保證、不搶佔執行中工作 |
| Explicit Synchronization | cudaDeviceSynchronize / cudaStreamSynchronize / cudaStreamWaitEvent / cudaStreamQuery 四種顯式同步粒度 |
| Implicit Synchronization | 某些 host 操作(記憶體配置、NULL stream 指令、L1/shared 切換等)會隱式序列化跨 stream 工作 |
| Events 表達相依 | 用 non-timing event + cudaStreamWaitEvent() 表達跨 stream 相依;只表達相依的 event 建議關閉 timing |
| PDL(相依啟動) | cudaTriggerProgrammaticLaunchCompletion() + cudaGridDependencySynchronize() + launch attribute,讓 primary 與 secondary kernel 部分重疊 |
More on Streams and Events(總覽)
CUDA stream 的基本語意:同一個 stream 上提交的操作預設被序列化(serialized),前一個未完成下一個不能開始。唯一的例外是新增的 Programmatic Dependent Launch(PDL) 功能。要啟用並行,可用多個 stream,也可用 CUDA Graphs,兩種方式還能結合。
不同 stream 的工作要能並行執行,需滿足特定條件:沒有 event 相依、沒有 implicit synchronization、硬體資源足夠等。
- 不同 stream 的獨立操作之間,若 host 對 NULL stream 提交任何 CUDA 操作,就無法並行——除非這些 stream 是 non-blocking stream。
- non-blocking stream:用
cudaStreamCreateWithFlags()搭配cudaStreamNonBlockingflag 建立。為提升並行潛力,建議一律建立 non-blocking stream。
永遠選「剛好滿足需求」、最不一般化的同步選項。
- 只需等某個 stream 全部完成 → 用
cudaStreamSynchronize(),勝過cudaDeviceSynchronize()(後者會多等 device 上所有 stream)。 - 需要不阻塞地等 → 用
cudaStreamQuery()在 polling loop 裡檢查回傳值。 - 同效果也可用 event:
cudaEventSynchronize()(阻塞)或cudaEventQuery()(非阻塞 polling)比cudaDeviceSynchronize()更聚焦。
用 Event 表達跨 stream 相依
- 表達 stream 之間的相依,建議使用 non-timing event:呼叫
cudaStreamWaitEvent(),讓某 stream 之後提交的操作等待先前(如另一 stream 上)記錄的 event 完成。 - CUDA event 預設帶 timing 資訊(可用於
cudaEventElapsedTime())。但若 event 只用來表達相依、不需計時,建議用cudaEventCreateWithFlags()搭配cudaEventDisableTimingflag 建立,以提升效能。
任何等待 / 查詢 event 的 CUDA API,使用者要自行確保 cudaEventRecord 已先被呼叫。未被 record 的 event 永遠回傳成功,可能造成「以為等到了其實沒等」的隱性 bug。
Explicit Synchronization(顯式同步)
四種不同粒度的顯式同步方法:
| API | 行為 / 粒度 |
|---|---|
cudaDeviceSynchronize() |
等到所有 host thread、所有 stream 的所有前置指令完成(最粗、最一般化) |
cudaStreamSynchronize(stream) |
等到指定 stream 的所有前置指令完成;其他 stream 可繼續在 device 上執行 |
cudaStreamWaitEvent(stream, event) |
讓該 stream 在此呼叫之後加入的指令,延後到指定 event 完成才執行(跨 stream 相依) |
cudaStreamQuery(stream) |
查詢該 stream 的前置指令是否全部完成(非阻塞) |
Table 4:與 host 的顯式同步選項摘要
| 等待對象 | Non-blocking(需 polling loop) | Blocking |
|---|---|---|
| 特定 stream | cudaStreamQuery() |
cudaStreamSynchronize() |
| 特定 event | cudaEventQuery() |
cudaEventSynchronize() |
| device 上所有工作 | N/A | cudaDeviceSynchronize() |
Implicit Synchronization(隱式同步)
來自不同 stream 的兩個指令,若 host thread 在它們之間發出下列任一操作,就無法並行:
- page-locked(pinned)host 記憶體配置
- device 記憶體配置
- device 記憶體 set(memset)
- 在同一 device 記憶體兩個位址之間的 memory copy
- 對 NULL stream 的任何 CUDA 指令
- L1 / shared memory 配置之間的切換
需要做相依檢查(dependency check)的操作包含:同一 stream 內、位於被檢查 launch 之前的任何指令,以及對該 stream 的任何 cudaStreamQuery() 呼叫。
- 所有獨立操作都在相依操作之前發出(All independent operations issued before dependent operations)。
- 任何同步都儘可能延後(delay synchronization as long as possible)。
host thread 時間軸(避免隱式序列化):
stream A: kA1 ──────────┐
stream B: kB1 ──────────┤ ← A、B 可並行
│
[中間插入 cudaMalloc / NULL-stream op / L1↔shared 切換]
▼
→ 此後 A、B 之間被隱式序列化(無法重疊)
Stream Priorities(Stream 優先級)
- 建立時用
cudaStreamCreateWithPriority()指定相對優先級。 - 可用優先級範圍由
cudaDeviceGetStreamPriorityRange()取得,順序為 [greatest priority, least priority]。 - runtime 時,GPU scheduler 用 stream priority 決定任務挑選順序,但這是 hint 而非保證。
- 挑選要 launch 的工作時,高優先級 stream 的待處理任務優先於低優先級。
- 高優先級任務不會搶佔(preempt)已在執行的低優先級任務。
- GPU 不會在執行中重新評估 work queue;提高某 stream 優先級不會中斷正在進行的工作。
// get the range of stream priorities for this device
int leastPriority, greatestPriority;
cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority);
// create streams with highest and lowest available priorities
cudaStream_t st_high, st_low;
cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, greatestPriority);
cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, leastPriority);
重點:先取得 [greatestPriority, leastPriority] 範圍,再分別建立最高 / 最低優先級的 non-blocking stream。
stream priority 只影響任務被挑選的順序,不提供嚴格排序保證,也不搶佔執行中工作。需要嚴格順序時必須改用 event / 同步,而非依賴 priority。
Programmatic Dependent Kernel Launch(PDL,相依 kernel 啟動)
CUDA stream 語意讓 kernel 依序執行:第二個 kernel 開始時,第一個 kernel 寫出的相依資料保證已就緒。但常見的情況是——第一個 kernel 已把後續所需資料寫入 global memory,卻還有其他工作要做;而第二個 kernel 在需要那份資料之前也有一段獨立工作。此時可讓兩個 kernel 部分重疊(假設硬體資源足夠),連第二個 kernel 的 launch overhead 都能一起被覆蓋掉。
可達成的重疊程度,除了硬體資源外,取決於 kernel 的具體結構:
- 第一個 kernel 在其執行的哪個時點完成第二個 kernel 所依賴的工作?
- 第二個 kernel 在其執行的哪個時點才開始用到第一個 kernel 的資料?
由於高度依賴具體 kernel,難以完全自動化,因此 CUDA 提供 PDL 讓開發者自行指定兩個 kernel 之間的同步點。
PDL 的三個組成
| 組成 | 由誰 / 用什麼 | 作用 |
|---|---|---|
| i) trigger | primary kernel 呼叫 cudaTriggerProgrammaticLaunchCompletion() |
宣告「我已完成 secondary kernel 所需的一切」 |
| ii) sync | secondary kernel 呼叫 cudaGridDependencySynchronize() |
宣告「我已做完獨立工作,現在要等 primary 完成相依部分」 |
| iii) launch | secondary 用 attribute cudaLaunchAttributeProgrammaticStreamSerialization,programmaticStreamSerializationAllowed = 1 |
啟用 PDL 的特殊啟動序列化 |
__global__ void primary_kernel() {
// Initial work that should finish before starting secondary kernel
// Trigger the secondary kernel
cudaTriggerProgrammaticLaunchCompletion();
// Work that can coincide with the secondary kernel
}
__global__ void secondary_kernel() {
// Initialization, Independent work, etc.
// Will block until all primary kernels the secondary kernel is dependent on
// have completed and flushed results to global memory
cudaGridDependencySynchronize();
// Dependent work
}
// Launch the secondary kernel with the special attribute
cudaLaunchAttribute attribute[1];
attribute[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
attribute[0].val.programmaticStreamSerializationAllowed = 1;
cudaLaunchConfig_t config = {0};
config.gridDim = grid_dim;
config.blockDim = block_dim;
config.dynamicSmemBytes = 0;
config.stream = stream;
config.attrs = attribute; // Add special attribute for PDL
config.numAttrs = 1;
// Launch primary kernel (normal launch)
primary_kernel<<<grid_dim, block_dim, 0, stream>>>();
// Launch secondary (dependent) kernel via launch config with the attribute
cudaLaunchKernelEx(&config, secondary_kernel);
重點:primary 用一般 <<<>>> 啟動;secondary 必須透過帶 PDL attribute 的 cudaLaunchConfig_t 用 cudaLaunchKernelEx() 啟動。cudaTriggerProgrammaticLaunchCompletion() 後到 grid 結束之間的工作,可與 secondary 的獨立段重疊。
PDL 時序(arrive / wait)
時間 ───────────────────────────────────────────────►
primary : [初始工作]──Trigger()──[可與 secondary 重疊的剩餘工作]──┐(flush 到 global mem)
│ │
▼ 觸發 secondary 可開始 │
secondary: [獨立工作]──GridDependencySync()··········▼ 解除阻塞──[相依工作]
▲ 在此阻塞,直到 primary 完成並 flush 結果
重疊區段:primary 的剩餘工作 ‖ secondary 的獨立工作(含 launch overhead 被覆蓋)
必須同時具備:primary 端 cudaTriggerProgrammaticLaunchCompletion()、secondary 端 cudaGridDependencySynchronize()、以及 secondary launch 時設定 programmaticStreamSerializationAllowed = 1 的 attribute。任一缺失就無法獲得正確的重疊語意。
能否真正重疊還取決於硬體資源是否足夠,以及兩個 kernel 各自「何時產出 / 何時開始消費」相依資料的結構。PDL 只提供同步點機制,不保證一定產生重疊。
考試/測驗重點
| 情境/關鍵字 | 答案 |
|---|---|
| 同一 stream 內的操作預設行為 | 序列化(serialized),唯一例外是 PDL |
| 想避免 NULL stream 阻斷並行 | 用 cudaStreamCreateWithFlags() + cudaStreamNonBlocking 建 non-blocking stream |
| 只等某一個 stream 完成該用哪個 API | cudaStreamSynchronize(),不要用 cudaDeviceSynchronize() |
| 非阻塞等待 stream / event | cudaStreamQuery() / cudaEventQuery()(放在 polling loop) |
| 跨 stream 表達相依 | cudaStreamWaitEvent(),建議用 non-timing event |
| event 只表達相依、不計時 | cudaEventCreateWithFlags() + cudaEventDisableTiming(提升效能) |
| 未被 record 的 event 查詢結果 | 永遠回傳 success(需自行確保已 cudaEventRecord) |
| stream priority 範圍順序 | cudaDeviceGetStreamPriorityRange() 回 [greatest, least] |
| 提高 stream priority 會搶佔執行中工作嗎 | 不會;只影響挑選順序、是 hint、不重評估 work queue |
| 隱式同步觸發來源(記憶體類) | pinned host alloc / device alloc / memset / 同 device 兩位址 memcpy |
| 隱式同步觸發來源(非記憶體類) | 對 NULL stream 的任何指令、L1/shared 配置切換 |
| 提升並行的兩準則 | 獨立操作先發、同步儘量延後 |
| PDL:primary 端呼叫 | cudaTriggerProgrammaticLaunchCompletion() |
| PDL:secondary 端呼叫 | cudaGridDependencySynchronize() |
| PDL:secondary launch attribute | cudaLaunchAttributeProgrammaticStreamSerialization,programmaticStreamSerializationAllowed = 1 |
| PDL secondary 用哪個 launch API | cudaLaunchKernelEx() 搭配 cudaLaunchConfig_t |
| PDL 能覆蓋什麼開銷 | secondary kernel 的 launch overhead 與部分執行時間 |
Related Notes
- 03-Advanced-CUDA/01-Advanced-Launch-and-Clusters
- 03-Advanced-CUDA/03-Batched-Transfers-and-Env-Vars
- 02-Programming-GPUs/14-Async-Streams-and-Events
- 02-Programming-GPUs/15-Async-Callbacks-Ordering-Graphs
- 02-Programming-GPUs/01-CUDA-Cpp-Kernels-and-Launch
- 03-Advanced-CUDA/Practice-Advanced-CUDA
- 00-Dashboard/Exam-Traps