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它)