進階 Streams 與相依啟動 (Advanced Streams and Dependent Launch)

重點總覽

項目 重點
Stream 序列化與並行 同一 stream 內預設序列化;唯一例外是 PDL。不同 stream 在無 event 相依、無 implicit sync、資源足夠時可並行
NULL stream 阻斷 中間若對 NULL stream 下任何指令,會阻斷不同 stream 並行;non-blocking streamcudaStreamNonBlocking)可避免
最小同步原則 選擇「剛好夠用」最不一般化的同步方式;偏好 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、硬體資源足夠等。

最小同步原則(least general synchronization)

永遠選「剛好滿足需求」、最不一般化的同步選項。

  • 只需等某個 stream 全部完成 → 用 cudaStreamSynchronize()勝過 cudaDeviceSynchronize()(後者會多等 device 上所有 stream)。
  • 需要不阻塞地等 → 用 cudaStreamQuery() 在 polling loop 裡檢查回傳值。
  • 同效果也可用 event:cudaEventSynchronize()(阻塞)或 cudaEventQuery()(非阻塞 polling)比 cudaDeviceSynchronize() 更聚焦。

用 Event 表達跨 stream 相依

未 record 的 event 永遠回傳 success

任何等待 / 查詢 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()
顯式同步的選擇若落在應用的 critical path 上特別重要——選錯粒度(例如用 device 級同步)會白白等待無關的 GPU 工作。

Implicit Synchronization(隱式同步)

來自不同 stream 的兩個指令,若 host thread 在它們之間發出下列任一操作,就無法並行

需要做相依檢查(dependency check)的操作包含:同一 stream 內、位於被檢查 launch 之前的任何指令,以及對該 stream 的任何 cudaStreamQuery() 呼叫。

提升並行的兩條準則

  1. 所有獨立操作都在相依操作之前發出(All independent operations issued before dependent operations)。
  2. 任何同步都儘可能延後(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 優先級)

// 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。

priority 是「影響」不是「強制排序」

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,難以完全自動化,因此 CUDA 提供 PDL 讓開發者自行指定兩個 kernel 之間的同步點

PDL 的三個組成

組成 由誰 / 用什麼 作用
i) trigger primary kernel 呼叫 cudaTriggerProgrammaticLaunchCompletion() 宣告「我已完成 secondary kernel 所需的一切」
ii) sync secondary kernel 呼叫 cudaGridDependencySynchronize() 宣告「我已做完獨立工作,現在要等 primary 完成相依部分」
iii) launch secondary 用 attribute cudaLaunchAttributeProgrammaticStreamSerializationprogrammaticStreamSerializationAllowed = 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_tcudaLaunchKernelEx() 啟動。cudaTriggerProgrammaticLaunchCompletion() 後到 grid 結束之間的工作,可與 secondary 的獨立段重疊。

PDL 時序(arrive / wait)

時間 ───────────────────────────────────────────────►

primary :  [初始工作]──Trigger()──[可與 secondary 重疊的剩餘工作]──┐(flush 到 global mem)
                          │                                        │
                          ▼ 觸發 secondary 可開始                   │
secondary:                [獨立工作]──GridDependencySync()··········▼ 解除阻塞──[相依工作]
                                          ▲ 在此阻塞,直到 primary 完成並 flush 結果
重疊區段:primary 的剩餘工作  ‖  secondary 的獨立工作(含 launch overhead 被覆蓋)
PDL 三要件缺一不可

必須同時具備: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 cudaLaunchAttributeProgrammaticStreamSerializationprogrammaticStreamSerializationAllowed = 1
PDL secondary 用哪個 launch API cudaLaunchKernelEx() 搭配 cudaLaunchConfig_t
PDL 能覆蓋什麼開銷 secondary kernel 的 launch overhead 與部分執行時間