CUDA Stream優化經驗

Multi-Process Service(MPS)原理:

    一個GPU卡上同時只能執行一個context;因此多進程同時往一個GPU卡上提交任務時,同時只能有一個任務跑起來,沒法多任務並行;

    MPS服務:多進程提交的任務先提交至MPS服務進程,該進程會把所有任務使用同一個context但不同的stream, 提交給該塊GPU卡,使得可以多任務並行;

    缺點:增大了任務提交的延遲,因爲要多經過MPS服務進程這個“代理”;

Stream: 任務隊列,單個Stream內部FIFO,多個Stream之間及和host之間可overlap執行;

銷燬Stream的API, host要blocking到該Stream所有任務執行完畢後,纔會執行成功並繼續;

儘量不要使用Stream0(defalut stream,有些API不指定stream則默認使用這個),它不能和其他stream並行執行;(其他stream設了cudaStreamNonBlocking者例外)

內存<-->顯存Copy要想異步,必須同時滿足以下條件:(另外,同方向同時只能有一個Copy在執行)

1. Copy任務必須不能在default stream裏;

2. 必須使用Async版本API(cudaMemcpyAsync);

3. Host內存必須是pinned的;

 

同步:

按“狠”的程度從高到低:

    1. cudaDeviceSynchronize: Host等所有stream的所有任務都執行結束,才繼續往下走;

    2. cudaStreamSynchronize: Host等這一個stream的所有任務都執行結束,才繼續往下走; 

    3. 使用Event來同步;可以讓Host等某個stream的某個event,也可以讓某個stream等另一個stream的event;

Event有2種狀態:發生,沒發生;創建時默認是“發生”,cudaEventRecord會把它設爲“沒發生”,在stream裏執行到它這裏會把它設爲“發生”;(多線程時,注意不要在創建event之前去調用cudaEventRecord,這裏易出bug)

如果創建時使用"cudaEventDisableTiming"這個Flag,則該Event被執行到時不進行時間記錄,可以節省開銷;(我認爲此時該event僅用於同步)

同步時對event的使用有3種方法:

    1. cudaEventQuery:Host主動查詢event的狀態;

    2. cudaEventSynchronize:Host blocking住,等待該event執行到才繼續走;

    3. cudaStreamSynchronize:另一個stream blocking住(Host繼續執行不blocking),等待該event執行到才繼續走;

CUDA_LAUNCH_BLOCKING=1環境變量可以讓所有stream變成對Host而言是同步執行(即Host發射一個任務,就等着該任務執行完,Host才能繼續往下走);用於debug時;

Profiling工具:

    Windows上:Nsight Visual Studio版;NVIDIA Visual Profiler;

    Linux上:Nsight Eclipse版;NVIDIA Visual Profiler; 命令行nvprof;

優化原則:

    1. 讓瓶頸設備的“空置率”儘可能低;

    2. Host線程發射任務儘可能早些,讓發射和實際執行之間的“空閒”間隔儘可能小;

常見bad-case:

1-A: 啓動kernel時忘記指定stream,導致默認stream和其他stream之間串行執行;

1-B: cudaEventRecord時忘記指定stream,導致該event被放進默認stream,導致cudaEventSynchronize時等待默認stream, 而默認stream又要等待其他steam的完成,造成大等待;

1:以上問題的解決:不要再默認stream上放任務;調用API要小心,不要忘記指定stream參數;或者讓其他stream創建時指定爲cudaStreamNonBlocking;

2-A:先cudaMemcpy,再啓動另一個stream上的kernel,導致後者等待前者;因爲前者放進了默認stream,要執行完Host才能繼續;解決:換成cudaMemcpyAsync交給其他stream即可;

2-B:cudaMemcpyAsync時忘記在主存上使用pinned memory,導致退化爲同步copy版本;(Visual Profiler上會顯示"Memory Type"是"Pageable")

3:<顯存開闢、kernel執行、顯存釋放>不斷迭代,導致kernel要等顯存開闢完成才執行,顯存釋放要等kernel執行完畢才執行(這兩者會自動被CUDA檢測到?示例代碼不會崩潰?);顯現出來痕跡是Host執行某些API的時間特別長(例如此例的cudaFree);解決:反覆重用顯存,減少開闢和釋放的次數;

4:Host幹活慢,耽誤了GPU stream的發射;解決:把活兒交給GPU去做,Host採用多線程/SIMD等技術來加速;

5:kernel執行時間太短,顯得Host端執行"cudaLaunch"的時間相對長,時間都浪費在"cudaLaunch"、"cudaLaunch"和真正執行之間的空隙上了;解決:"融合"成大任務、batch,總之讓kernel耗時更長些;

6:過度同步:Host很多時候等待在同步API上;解決:合理重構儘量少同步、使用更”不狠“的同步API例如cudaEventSynchronize少用cudaDeviceSynchronize;

7:Profiler開銷:減少同步次數和程度?

8:古老的CUDA GPU架構裏,所有stream的任務被放到同一個隊列的,所以並行程度和任務發射的順序有關;

stream callback:

新一些的CUDA支持"cudaStreamAddCallback",在該stream執行到這裏時,在host端調用這個指定的callback函數;可用於當stream的某任務完成時,通知Host端做些事;

注意:所有stream上註冊的callback,都會被同一個driver線程執行,因此是串行的(神似ps-lite的用戶callback!);所以callback裏儘量只放很輕量級的操作,例如把指針交給線程池裏的某個線程並signal它)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章