關於Numba的執行緒實現的說明

wujianming_110117發表於2020-12-27

關於Numba的執行緒實現的說明
由Numbaparallel目標執行的工作由Numba執行緒層執行。實際上,“執行緒層”是Numba內建庫,可以執行所需的併發執行。在撰寫本文時,有三個可用的執行緒層,每個執行緒層都通過不同的較低階別的host執行緒庫實現。上thread執行緒和對於給定的應用/系統的thread執行緒的適當選擇的更多資訊可在發現 threading layer documentatio。
以下各節要關注的相關資訊是,執行並行執行緒庫中的parallel_for函式。此功能的工作是協調和執行並行任務。
本文引用的相關原始檔是
• numba/np/ufunc/tbbpool.cpp
• numba/np/ufunc/omppool.cpp
• numba/np/ufunc/workqueue.c
這些檔案分別包含TBB,OpenMP和工作佇列執行緒池實現。每個函式都包含函式 set_num_threads(),get_num_threads()和和get_thread_id(),以及在各自的排程程式中用於執行緒遮蔽的相關邏輯。請注意,基本執行緒區域性變數邏輯在這些檔案中都是重複的,並且不在它們之間共享。
• numba/np/ufunc/parallel.py
該檔案包含了Python和JIT相容的封裝器 set_num_threads(),get_num_threads()和get_thread_id(),以及程式碼載入上述庫到Python和啟動執行緒池。
• numba/parfors/parfor_lowering.py
該檔案包含用於為並行後端生成程式碼的主要邏輯。在生成排程程式程式碼的程式碼中訪問該執行緒掩碼,並將其傳遞給相關的後端排程程式功能。
執行緒遮蔽
作為其設計的一部分,Numba絕不會在numba.np.ufunc.parallel._launch_threads() 執行首次並行執行時,最初啟動的執行緒之外啟動新執行緒。這是由於在實施執行緒遮蔽之前已在Numba中實現了執行緒。保留此限制是為了使設計簡單,儘管將來可以刪除它。因此,可以以程式設計方式設定執行緒數,但只能設定為小於或等於已啟動的總數。這是通過“遮蔽”未使用的執行緒來完成的,從而使它們不起作用。例如,在16核計算機上,如果使用者要呼叫set_num_threads,則Numba將始終存在16個執行緒,但是其中12個執行緒將處於空閒狀態以進行平行計算。進一步呼叫 set_num_threads(16) ,導致這些相同的執行緒在以後的計算中起作用。
新增了執行緒掩碼,使使用者可以以程式設計方式更改線上程層中執行工作的執行緒數。事實證明,執行緒遮蔽難以實現,需要開發一種適合使用者,易於推理且可以安全實現的程式設計模型,並在各個執行緒層上具有一致的行為。
程式設計模型
選擇的程式設計模型與OpenMP中的模型相似。做出此選擇的原因是,它對於許多使用者來說是熟悉的,範圍有限且簡單。通過呼叫指定正在使用 set_num_threads的執行緒數,可以通過呼叫查詢正在使用的執行緒數 get_num_threads。這兩個函式與它們的OpenMP對應(與上述限制相同,即掩碼必須小於或等於已啟動的執行緒)。執行語義也與OpenMP相似,因為一旦啟動並行區域,更改執行緒掩碼不會對當前正在執行的區域產生影響,但會對隨後執行的並行區域產生影響。
實施
除了對執行緒層庫中已經存在的使用者程式碼進行限制以外,對使用者程式碼沒有其它限制,需要仔細考慮執行緒掩碼的設計。無法將“執行緒掩碼”儲存在全域性值中,因為同時使用執行緒層,可能會導致值本身出現競爭形式。涉及具有這種全域性價值的各種互斥量的眾多設計,最終僅通過理想實驗就打破了所有這些互斥量。最終發現,在某些OpenMP實現之後,“執行緒掩碼”最好以a實現。這意味著每個執行Numba並行函式的執行緒,都將具有一個執行緒本地儲存(TLS)插槽,其中包含線上程中排程執行緒thread localparallel_for時,要使用的執行緒掩碼的值。
TLS使用一個執行緒掩模的上述概念是相對容易實現的, get_num_threads和set_num_threads簡單地需要解決的TLS時隙在給定的執行緒層。這也意味著可以從執行時呼叫中得出並行區域的執行排程get_num_threads。這是通過眾所周知的且相對容易實現的C 庫函式註冊模式,並將其包裝在內部Numba來實現的。
除了滿足原始的前期執行緒遮蔽要求之外,還需要考慮以下一些更復雜的方案。
巢狀並行
在所有執行緒層中,“主執行緒”將呼叫該parallel_for 函式,然後在並行區域中,根據執行緒層的不同,一些其它執行緒將有助於完成實際工作。如果工作包含對另一個並行函式的呼叫(即巢狀並行性),則使呼叫的執行緒必須知道主執行緒的“執行緒掩碼”是什麼,以便可以將其傳遞到主執行緒中。 parallel_for在執行巢狀並行函式時呼叫。此行為的實現是特定於執行緒層的,但一般原則是“主執行緒”始終將執行緒掩碼的值從其TLS插槽“傳送”到執行緒層中,在並行區域中處於活動狀態的所有執行緒。這些活動執行緒將在執行任何工作之前,使用此值更新其TLS插槽。該實現細節的最終結果是:
• 執行緒掩碼正確傳遞到巢狀函式中
• 並行區域中的每個執行緒仍然可以安全地使用不同的掩碼來呼叫巢狀函式,如果未明確設定,則使用從“主執行緒”繼承的掩碼
• parallel_for成功容納具有動態排程,且執行緒可能在執行期間加入和離開活動池的執行緒層
• 任何“主執行緒”執行緒掩碼都與活動執行緒池中執行緒的執行緒掩碼的流入特性完全分離
Python執行緒獨立呼叫並行函式
嚴格保護執行緒層啟動順序,確保啟動既是執行緒安全的,又是程式安全的,並且每個程式執行一次。在具有大量threading都使用Numba的Python模組執行緒的系統中,啟動序列中的第一個執行緒將正確設定其執行緒掩碼,但是沒有其他執行緒可以執行啟動序列。這意味著其它執行緒將需要以其它方式設定其初始執行緒掩碼。這是在get_num_threads呼叫,且不存線上程掩碼時實現的,在這種情況下,執行緒掩碼將設定為預設值。在該實現中,“不存線上程掩碼”由值表示,-1。“預設執行緒掩碼”(未設定)由值表示0。執行set_num_threads(NUMBA_NUM_THREADS),此運算元也會立即呼叫,因此如果有-1或0,由於遇到此結果get_num_threads(),而表示上述過程中存在錯誤。
作業系統fork()呼叫
使用TLS也是在由Linux(用於Numba使用最流行的平臺)驅動,將TLS傳輸到子程式fork(2, 3P)clone(2)CLONE_SETTLS。
執行緒ID
私有get_thread_id()函式被新增到每個執行緒後端,該函式為每個執行緒返回唯一的ID。可以通過以下方式從Python訪問 numba.np.ufunc.parallel._get_thread_id()(也可以在JIT編譯函式中使用它)。執行緒ID函式對於測試執行緒遮蔽行為是否正確很有用,但不應在測試之外使用。例如,可以呼叫set_num_threads,收集_get_thread_id()並行區域中的所有唯一性,驗證僅執行了4個執行緒。
注意事項
測試執行緒遮蔽時需要注意的一些注意事項:
• TBB後端可以選擇排程少於給定掩碼數的執行緒。因此,諸如上述測試,可能返回少於4個唯一執行緒。
• 工作佇列後端不是執行緒安全的,因此嘗試對其執行多執行緒巢狀並行處理,可能會導致死鎖或其他未定義的行為。如果工作佇列後端檢測到巢狀的並行性,它將發出SIGABRT訊號。
• 某些後端可能會重用主執行緒進行計算,但是不應依賴此行為(例如,如果傳遞異常)。
在程式碼生成中使用
get_num_threads在程式碼生成中使用的一般模式是
import llvmlite.llvmpy.core as lc

get_num_threads = builder.module.get_or_insert_function(
lc.Type.function(lc.Type.int(types.intp.bitwidth), []),
name=“get_num_threads”)

num_threads = builder.call(get_num_threads, [])

with cgutils.if_unlikely(builder, builder.icmp_signed(’<=’, num_threads,
num_threads.type(0))):
cgutils.printf(builder, “num_threads: %d\n”, num_threads)
context.call_conv.return_user_exc(builder, RuntimeError,
("Invalid number of threads. "
“This likely indicates a bug in Numba.”,))

# Pass num_threads through to the appropriate backend function here
請參閱中的程式碼numba/parfors/parfor_lowering.py。
num_threads嚴格禁止<= 0的防護措施是必要的,但是線上程遮蔽邏輯包含錯誤的情況下,它可以防止意外的錯誤行為。
該num_threads變數應傳遞給適當的後端函式,例如do_scheduling或parallel_for。如果以其它方式使用(而不是將其傳遞給後端函式),應考慮上述注意事項,確保num_threads安全使用變數。最好將這樣的邏輯保留線上程後端中,而不是嘗試在程式碼生成中進行。

相關文章