點內(nèi)部嵌套算法與嵌套流圖)
并發(fā)編程高性能計算【免費下載鏈接】oneTBBoneAPI Threading Building Blocks (oneTBB)項目地址https://gitcode.com/gh_mirrors/on/oneTBB點擊查看免費下載導(dǎo)讀本文圍繞 oneTBBoneAPI Threading Building BlocksFlow Graph 的嵌套并行技巧展開講解兩種核心做法在節(jié)點體內(nèi)嵌套其他并行算法如parallel_for以提升可擴展性以及在節(jié)點體內(nèi)構(gòu)造并執(zhí)行嵌套的流圖。讀完本文后你將掌握如何把 Flow Graph 當作一種協(xié)調(diào)語言——在圖層面表達粗粒度并行在節(jié)點內(nèi)部表達細粒度并行——并學(xué)會在嵌套圖結(jié)構(gòu)不變時通過復(fù)用持久化圖來消除不必要的重建開銷。原文出自倉庫文檔 Flow_Graph_nested_parallelism_tips.rst包含 use_nested_algorithms.rst 與 use_nested_flow_graphs.rst 兩篇姊妹篇。為什么要嵌套并行把 Flow Graph 當作協(xié)調(diào)語言提高 Flow Graph 可擴展性的一條強大途徑是在節(jié)點體node body內(nèi)部嵌套其他并行算法。這樣做的意義在于你可以把 Flow Graph 用作一種協(xié)調(diào)語言coordination language——在圖的層面表達最粗粒度的并行在節(jié)點內(nèi)部嵌套更細粒度的并行從而把任務(wù)如何組合與單個任務(wù)內(nèi)部如何并行兩個層次解耦。從實現(xiàn)上看這種設(shè)計之所以可行是因為 Flow Graph 的節(jié)點執(zhí)行與底層 oneTBB 任務(wù)調(diào)度器深度融合節(jié)點體最終被封裝為圖任務(wù)graph task提交到與圖關(guān)聯(lián)的任務(wù)競技場task arena中執(zhí)行見 src/tbb/task_dispatcher.cpp 與 include/oneapi/tbb/detail/_flow_graph_impl.h 中的graph_task。因此當節(jié)點體內(nèi)部再調(diào)用parallel_for等并行算法時這些算法生成的子任務(wù)會被同一調(diào)度器無縫接納實現(xiàn)嵌套并行的自然展開。技巧一在節(jié)點體內(nèi)嵌套并行算法場景與圖結(jié)構(gòu)文檔給出的示例是一個典型的生產(chǎn)者–變換–消費者流水線由五個節(jié)點組成一個input_nodematrix_source從一個文件按順序讀取矩陣序列兩個function_noden1、n2接收矩陣并對每個元素應(yīng)用一個函數(shù)f1/f2生成兩個新矩陣兩個終結(jié)點function_noden1_sink、n2_sink分別消費n1和n2產(chǎn)出的結(jié)果矩陣。連接關(guān)系為matrix_source同時連接n1與n2廣播n1連接n1_sinkn2連接n2_sink。其中read_next_matrix、f1、f2、consume_f1、consume_f2等函數(shù)在原文中未給出需由讀者自行實現(xiàn)例如對矩陣逐元素應(yīng)用f1后返回新矩陣consume_*負責(zé)釋放或匯總結(jié)果。完整代碼示例以下代碼完整復(fù)刻原文示例其中的關(guān)鍵點是在n1和n2的 lambda 表達式中嵌套了parallel_for對矩陣的每個元素并行執(zhí)行變換graph g; input_node double * matrix_source( g, - double* { double *a read_next_matrix(); if ( a ) { return a; } else { fc.stop(); return nullptr; } } ); function_node double *, double * n1( g, unlimited, - double * { double *b new double[N]; parallel_for( 0, N, { b[i] f1(a[i]); } ); return b; } ); function_node double *, double * n2( g, unlimited, - double * { double *b new double[N]; parallel_for( 0, N, { b[i] f2(a[i]); } ); return b; } ); function_node double *, double * n1_sink( g, unlimited, []( double *b ) - double * { return consume_f1(b); } ); function_node double *, double * n2_sink( g, unlimited, []( double *b ) - double * { return consume_f2(b); } ); make_edge( matrix_source, n1 ); make_edge( matrix_source, n2 ); make_edge( n1, n1_sink ); make_edge( n2, n2_sink ); matrix_source.activate(); g.wait_for_all();關(guān)鍵 API 的底層語義input_node與flow_controlinput_node是無前驅(qū)、只做數(shù)據(jù)源的可執(zhí)行節(jié)點定義于 include/oneapi/tbb/flow_graph.h。它創(chuàng)建后默認處于未激活狀態(tài)需要顯式調(diào)用activate()才會開始向后繼節(jié)點投遞消息——這正是示例末尾matrix_source.activate()的含義。節(jié)點體接收一個oneapi::tbb::flow_control 參數(shù)其實現(xiàn)位于 include/oneapi/tbb/detail/_pipeline_filters.h內(nèi)部維護bool is_pipeline_stopped調(diào)用fc.stop()即置位該標志input_node在調(diào)用完節(jié)點體后檢查該標志若已停止則不再緩存/投遞本次返回的nullptr見try_reserve_apply_body中對control.is_pipeline_stopped的判斷flow_graph.h。因此示例中讀到結(jié)尾返回nullptr并fc.stop()是標準的停止輸入流的模式。unlimited并發(fā)度function_node的構(gòu)造參數(shù)concurrency決定節(jié)點體可以被多少個并發(fā)調(diào)用。unlimited與serial定義于 include/oneapi/tbb/flow_graph.henum concurrency { unlimited 0, serial 1 };。使用unlimited意味著每個到達的消息都可以立即啟動一個新的節(jié)點體執(zhí)行節(jié)點之間、以及節(jié)點體內(nèi)部的嵌套并行算法之間可以充分重疊——這是本例中兩個矩陣分支能夠并行推進、同時每個矩陣內(nèi)部又能再拆成多路并行的前提。function_node的構(gòu)造與內(nèi)部function_input/function_output的實現(xiàn)參見 flow_graph.h。嵌套算法的并行度疊加示例里n1與n2是unlimited的兩者可以并行各自內(nèi)部的parallel_for(0, N, ...)又會把單矩陣的逐元素變換進一步切分給多個工作線程。整體效果是圖層面并行 × 節(jié)點內(nèi)部并行共同壓滿硬件線程。需要留意的是節(jié)點體內(nèi)創(chuàng)建的新數(shù)組b由節(jié)點負責(zé)釋放示例中由consume_f1/consume_f2處理這是開發(fā)者需要自行保證的內(nèi)存生命周期約定。技巧二嵌套流圖Flow Graph 內(nèi)嵌 Flow Graph除了在節(jié)點體內(nèi)嵌套算法還可以在節(jié)點體內(nèi)嵌套整個流圖外層圖g的兩個節(jié)點a、b各自在收到消息時構(gòu)造并執(zhí)行一個內(nèi)層圖。節(jié)點a收到消息后構(gòu)造并執(zhí)行一個內(nèi)層依賴圖dependence graph其節(jié)點以continue_msg為消息類型通過make_edge形成n1 → n2、n1 → n3、n2 → n4、n3 → n4的菱形依賴結(jié)構(gòu)節(jié)點b收到消息后構(gòu)造并執(zhí)行一個內(nèi)層數(shù)據(jù)流圖data flow graph由四個function_nodem1m4以相同的菱形拓撲連接并注入整數(shù)消息。完整代碼示例graph g; function_node int, int a( g, unlimited, []( int i ) - int { graph h; node_t n1( h, { cout n1: i \n; } ); node_t n2( h, { cout n2: i \n; } ); node_t n3( h, { cout n3: i \n; } ); node_t n4( h, { cout n4: i \n; } ); make_edge( n1, n2 ); make_edge( n1, n3 ); make_edge( n2, n4 ); make_edge( n3, n4 ); n1.try_put(continue_msg()); h.wait_for_all(); return i; } ); function_node int, int b( g, unlimited, []( int i ) - int { graph h; function_node int, int m1( h, unlimited, []( int j ) - int { cout m1: j \n; return j; } ); function_node int, int m2( h, unlimited, []( int j ) - int { cout m2: j \n; return j; } ); function_node int, int m3( h, unlimited, []( int j ) - int { cout m3: j \n; return j; } ); function_node int, int m4( h, unlimited, []( int j ) - int { cout m4: j \n; return j; } ); make_edge( m1, m2 ); make_edge( m1, m3 ); make_edge( m2, m4 ); make_edge( m3, m4 ); m1.try_put(i); h.wait_for_all(); return i; } ); make_edge( a, b ); for ( int i 0; i 3; i ) { a.try_put(i); } g.wait_for_all();為什么必須調(diào)用h.wait_for_all()在第一種實現(xiàn)中內(nèi)層圖h是節(jié)點體作用域內(nèi)的局部變量每次調(diào)用都會在離開作用域時被析構(gòu)。而 oneTBB 的graph析構(gòu)函數(shù)本身會調(diào)用wait_for_all()等待圖中所有未完成任務(wù)執(zhí)行完畢見 include/oneapi/tbb/detail/_flow_graph_impl.h 中g(shù)raph::~graph()的實現(xiàn)。因此若節(jié)點體結(jié)束時不顯式調(diào)用h.wait_for_all()內(nèi)層圖的析構(gòu)會隱式等待語義上仍然安全但顯式調(diào)用h.wait_for_all()能讓等待內(nèi)層圖空閑這一意圖更加清晰并讓b的節(jié)點體阻塞至內(nèi)層圖完成從而保證返回i時內(nèi)層圖已徹底結(jié)束便于測試與排錯。graph::wait_for_all的實現(xiàn)依賴于圖內(nèi)置的等待計數(shù)機制reserve_wait/release_wait見 flow_graph_impl.h圖不會從wait_for_all返回直到所有reserve_wait都有對應(yīng)的release_wait與之匹配。依賴圖 vs 數(shù)據(jù)流圖兩種內(nèi)層圖的差異上述示例有意展示了兩種內(nèi)層圖依賴圖節(jié)點以continue_msg驅(qū)動n1.try_put(continue_msg())。continue_msg在 flow_graph.h 中定義為一個空標記類型continue_receiver則負責(zé)維護前驅(qū)計數(shù)只有所有前驅(qū)都投遞過continue_msg后節(jié)點才會執(zhí)行flow_graph.h。它表達的語義是依賴滿足即觸發(fā)適合建模 DAG 式的任務(wù)依賴關(guān)系數(shù)據(jù)流圖節(jié)點以真實的整數(shù)消息驅(qū)動m1.try_put(i)消息沿邊逐級變換傳遞適合建模數(shù)據(jù)加工管線。兩者都是嵌套圖的有效形態(tài)選擇哪一種取決于內(nèi)層任務(wù)之間的耦合方式是依賴關(guān)系還是數(shù)據(jù)流動。技巧三復(fù)用持久化內(nèi)層圖消除重建開銷如果嵌套圖的結(jié)構(gòu)在節(jié)點的多次調(diào)用之間保持不變那么每次調(diào)用都重新構(gòu)造一遍圖是冗余的——重建只會在執(zhí)行上增加不必要的開銷。文檔對此給出了優(yōu)化方案把內(nèi)層圖提升為外層作用域的持久對象節(jié)點b每次調(diào)用時直接向已存在的圖投遞消息。復(fù)用版完整代碼示例graph h; function_node int, int m1( h, unlimited, []( int j ) - int { cout m1: j \n; return j; } ); function_node int, int m2( h, unlimited, []( int j ) - int { cout m2: j \n; return j; } ); function_node int, int m3( h, unlimited, []( int j ) - int { cout m3: j \n; return j; } ); function_node int, int m4( h, unlimited, []( int j ) - int { cout m4: j \n; return j; } ); make_edge( m1, m2 ); make_edge( m1, m3 ); make_edge( m2, m4 ); make_edge( m3, m4 ); graph g; function_node int, int a( g, unlimited, []( int i ) - int { graph h; node_t n1( h, { cout n1: i \n; } ); node_t n2( h, { cout n2: i \n; } ); node_t n3( h, { cout n3: i \n; } ); node_t n4( h, { cout n4: i \n; } ); make_edge( n1, n2 ); make_edge( n1, n3 ); make_edge( n2, n4 ); make_edge( n3, n4 ); n1.try_put(continue_msg()); h.wait_for_all(); return i; } ); function_node int, int b( g, unlimited, - int { m1.try_put(i); h.wait_for_all(); // 可選h 不會被析構(gòu) return i; } ); make_edge( a, b ); for ( int i 0; i 3; i ) { a.try_put(i); } g.wait_for_all();復(fù)用后wait_for_all變得可選文檔特別指出在復(fù)用版中只有當你希望b的節(jié)點體阻塞等待內(nèi)層圖執(zhí)行完畢時才需要在每次調(diào)用末尾調(diào)用h.wait_for_all()。因為在第一版實現(xiàn)中圖h在離開作用域時被析構(gòu)析構(gòu)會隱式等待而復(fù)用版中h是持久對象不會在調(diào)用結(jié)束時被銷毀所以即使只調(diào)用m1.try_put(i)后直接返回、不等待h變?yōu)榭臻e也是合法的——內(nèi)層圖的消息會在后臺繼續(xù)執(zhí)行。這一優(yōu)化有兩個實踐要點生命周期管理持久圖h的存活時間必須覆蓋外層圖g的全部使用周期例如兩者都定義為main作用域內(nèi)的局部變量或具有合適的對象生命周期避免懸垂引用并發(fā)安全如果外層圖允許b的多個副本并發(fā)執(zhí)行unlimited并發(fā)度那么對同一個持久內(nèi)層圖h的并發(fā)try_put需要自行評估其線程安全性若需要串行化對內(nèi)層圖的訪問應(yīng)把b的并發(fā)度設(shè)為serial或在內(nèi)層圖訪問外加鎖。測試與驗證依據(jù)倉庫中與本文主題相關(guān)的驗證素材包括流圖節(jié)點基礎(chǔ)行為測試test/tbb/test_flow_graph.cpp 與 test/tbb/test_function_node.cpp覆蓋function_node、input_node、make_edge的消息投遞與并發(fā)語義嵌套復(fù)合節(jié)點測試test/tbb/test_composite_node.cpp 中的test_nested_adderL296驗證了復(fù)合節(jié)點內(nèi)部再嵌套節(jié)點的場景可作為嵌套結(jié)構(gòu)用法的補充參考嵌套并行與任務(wù)上下文測試test/tbb/test_eh_algorithms.cpp 討論了嵌套parallel_for/parallel_reduce與任務(wù)組上下文的關(guān)系test/tbb/test_arena_priorities.cpp 則包含嵌套 arena相關(guān)場景的測試L355-L372印證了 oneTBB 對多層嵌套并行執(zhí)行的支持。小結(jié)oneTBB Flow Graph 的嵌套并行提供了兩種互補的擴展手段手段適用場景關(guān)鍵注意點節(jié)點體內(nèi)嵌套并行算法parallel_for等單個節(jié)點內(nèi)部存在可并行的細粒度計算節(jié)點并發(fā)度設(shè)為unlimited以充分重疊注意節(jié)點體內(nèi)內(nèi)存的分配與釋放節(jié)點體內(nèi)嵌套流圖節(jié)點需要表達一組內(nèi)部任務(wù)之間的依賴/數(shù)據(jù)關(guān)系內(nèi)層圖被析構(gòu)時會隱式wait_for_all顯式等待以阻塞節(jié)點體復(fù)用持久化內(nèi)層圖內(nèi)層圖結(jié)構(gòu)在多次調(diào)用間不變僅在需要阻塞時調(diào)用h.wait_for_all()注意生命周期與并發(fā)訪問把粗粒度并行交給圖的拓撲把細粒度并行交給節(jié)點體內(nèi)的算法再把結(jié)構(gòu)固定的嵌套圖持久化復(fù)用——這套組合拳能讓你在保持圖結(jié)構(gòu)清晰的同時最大化硬件資源的利用率。更多流圖與嵌套并行的背景知識可繼續(xù)閱讀倉庫中的 Flow_Graph.rst、Nodes.rst 與 Guiding_Task_Scheduler_Execution.rst 等文檔。贊分享并發(fā)編程高性能計算【免費下載鏈接】oneTBBoneAPI Threading Building Blocks (oneTBB)項目地址https://gitcode.com/gh_mirrors/on/oneTBB點擊查看免費下載相關(guān)推薦FAIR Chemistry 生成模型全景ADiT、FlowMM、FlowLLM 與 Crystal-text-llm 的分子與材料生成技術(shù)指南FAIR Chemistry 生成模型全景ADiT、FlowMM、FlowLLM 與 Crystal text llm 的分子與材料生成技術(shù)指南 本文系統(tǒng)梳理并發(fā)編程高性能計算mold 內(nèi)嵌 oneTBB 并行基石task_group_context 取消與嵌套并行深度解析mold 內(nèi)嵌 oneTBB 并行基石task_group_context 取消與嵌套并行深度解析 mold 鏈接器的并行加速高度依賴內(nèi)嵌的 oneTBB見開發(fā)工具構(gòu)建工具系統(tǒng)編程oneTBB 嵌套并行取消機制詳解task_group_context、隔離上下文與流圖取消傳播oneTBB 嵌套并行取消機制詳解task_group_context、隔離上下文與流圖取消傳播 導(dǎo)讀 本文圍繞 oneTBBoneAPI Threadin并發(fā)編程高性能計算上一篇Changes架構(gòu)設(shè)計原理分布式構(gòu)建協(xié)調(diào)系統(tǒng)實現(xiàn)詳解 下一篇DLSS Swapper 完整指南:游戲 DLSS 版本管理一鍵切換不折騰創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考