code

顯示具有 Parallel Programming 標籤的文章。 顯示所有文章
顯示具有 Parallel Programming 標籤的文章。 顯示所有文章

2017年8月14日 星期一

Parallel Java 5 - Phaser

Barrier in Java

加入barrier會也是需要overhead,例如以下parallel code:



如果要確保所有Hello印出在所有Bye之前,我們插入barrier的位置可以在lookup(i)前或是後,但是這樣一定會讓整個parallel program的span = 200 (記得span就是parallel program中work最大的path)。

node ---> lookup ----> next

如果能夠把lookup和barrier也parallelize 就能降低span到100,類似以下?

node ------------> lookup
         --(join)---> barrier

這事實上是可以的,因為lookup的執行不需要等待barrier的執行,反過來說也是。
Java Phaser class提供這種所謂 split-phase barrier的機制:



Point to Point Synchronization

要壓榨barrier parallelism,必須考慮parallel program的結構,分析哪些statement是dependent on另外statement,不用一致性的等待擋住,以下為例,如果不考慮個別dependency的話,span = 6,因為barrier一刀橫在中間,使得phase 1 work (3) + phase 2 work (3) = 6。


這三個tasks的兩個phases的statements dependency列出來之後發現,可以fine-grained來設定barrier,Java也提供這樣的機制:



Pipeline

想像以下pipeline:


image processing分成好p個步驟,Denoise -> Registration -> Segmentation,這幾個步驟都必須等待前一步驟完成,所以是sequential。

如果有n張image,則同一個時間點可以parallel進行不同image的各個階段,所以n張image經過p個stage pipeline總共的parallel work = n*p (work的定義是所有cost的總和

computation graph:
D1 --join--> R1 --join--> S1
|fork
D2 --join--> R2 --join--> S2


span (CPL) = n 次Denoise  + 最後一張image 的剩下步驟只能sequentially complete: p - 1 work,所以總共是 n + p - 1

parallelism = work/span = n*p / (n+p-1) ,這邊可以假設 n >> p。所以 parallelism ~= p
也就是大量input的pipeline能獲得平行處理的加速,約等同於pipeline stages的數目。

如果用barrier可以很正確執行上面的semantics:





2017年8月11日 星期五

Parallel Java 4 - Loops

Forall Construct (Java沒有 @@

forall是for loop的parallel版本,可以看到semantics如下:


範例:
forall (i : [0:n-1]) a[i] = b[i] + c[i]

如果只對一個collection做事的話,也可以用stream。

forall (n) 會spawn n tasks,所以使用forall來設計parallel program的話,要盡量減少tasks數目,不能隨便使用。

Matrix Multiplication Example

用數學定義來做matrix multiplication的話:


會是一個triple loop:
對C中每個row I來說
對row I中每個column J說
對C[I][J]要loop相對應的A[I][all K] 以及 B[all K][J]

可以把計算每個C[i][J]的task parallelize,所以,所以計算C[0][0] 和 C[0][1]是independent的,那有辦法把裡面的K-loop parallelize嗎?


K-loop是對某特定C[I][J]做累加,如果不做任何synchronization的話,會產生data race。不過這不是concurrent programming,所以當然不會做synchronization,要不然就失去parallelism。

所以K-loop需要sequential execution。


Phase Barrier Construct in Forall

barrier是一個forall中使用的construct,可以把forall中的code分成好幾個phase,所有parallel execution都會被擋在barrier前,不能往下執行,直到所有的parallel execution都完成phase 1的code。

癌舉例來說,以下的兩種寫法都能得到一樣的Jacobi algorithm答案:


第一個方法顯而易見產生更多的tasks。
第二種方法把forall變成outer loop,但是由於有swap array pointer的需要,我們必須確保在swap pointer之前,所有的tasks都完成computation。此時需要barrier的協助,注意barrier是放入在inner sequential loop中

雖然這邊會instantiate 和第一個方法#tasks 一樣多的barriers,但是barriers的overhead遠比task coordination少得多,所以這也就是能夠比第一種方法加速的原因。

避免產生太多tasks!

由上面那一段可以知道,由於實體的cpu core數目相對於data數目總是太少(例如16 cores vs 1 billion elements),如果把每個element 都parallel去處理,反而會拖慢速度,因為overhead >> 加速性。

所以類似之前做法,要把1 billion elements拆成幾個 n parts,那些n parts就可以在inner sequential loop處理掉,而outer loop就可以用forall parallelize,這樣tasks的數目就減少到n個。

n 的數目要實驗,因為會依據環境而定。

至於怎麼拆法,那就不一定,看選擇。


2017年8月10日 星期四

Parallel Java 3 - Memoization and Streams

Memoization

這是一種functional programming optimization的方法。

在sequential FP中,這其實就是建立lookup table,類似dynamic programming的方法:


parallelization版本中,lookup table應該要存放的是future objects,這樣才不會馬上去compute tasks,但是一旦compute之後,就不會再做第二次:




Java Streams

簡單來說,就是把java collection轉換成functional programming的collection,所以一堆filter/map/reduce之類的function都可以用,對parallelization來說,跟scala依樣,只要加上個par就可以把stream轉換成parallel stream,所以所有的filter/map/reduce之類的operation都會run in parallel。



Determinism

首先定義
1. functional determinism: 如果一個parallel prorgam,同一個input永遠會有同一個output
2. structural determinism: 如果一個parallel program,同一個input永遠會有同一個computation graph
3. determinism: 1. 2.同時成立

parallel program最常見的問題就是data race,如果使用本課程所教的parallel programming constructs的話,保證是deterministic!

就這樣  @@









Parallel Java 2 - Future

Functional Programming

functional programming最大特點就是沒有state,也就是每次f(x) = y 是固定的,不可能有其他mapping (否則就違反 function 在數學上的定義)。

所以一但釐清function calls之間的dependency,則可以找到能夠parallelize的部分。例如以下三個function calls:


C = G(A), D = H(A),所以只要知道A的值,C和D的運算式可以平行的。


Future Model

Future是一個functional parallelism pattern,定義某個async object的functional operations,當建立物件時,不會立即執行task,而是當caller 呼叫 get() ,才會去執行task (可以看成function才真正去把input map成output,所以稱為Future。

例如上面說的 A = F(B) 這個mapping,如果用Future來表現 (以下是pseudocode):

FA = Future { F(B) },這邊宣告建立一個Future instance (也稱為promise),其執行的task為 F(B)。
則當要真的去取得FA的task結果,我們可以 FA.get()。

所以 宣告 FC = Future{ G(FA.get()) },FD = Future { H(FA.get()) }

注意因為Future要再get()被呼叫時才會真正去執行task 並且block and wait,所以它在宣告後可以馬上return,繼續執行下一個statement。如果把第一段三個數學式子化成computation graph:


可以看到一但G(A) H(A)要真正執行task的時候,他們要等待FA這個FUTURE執行完,也F(B)的value必須要得到,所以在graph中是以JOIN來block。


Implement Future model in Java

因為Future task是有return value,所以要extend RecursiveTask。
依樣要override compute(),其他沒什麼不一樣。

真正要create future object還是依樣要call fork(),主要的thread還是要join,這相當於implement Future中的get():


注意這邊的join()是有return value的,因為RecursiveTask有implement Java的Future interface,所以這個join()跟之前的RecursiveAction的join是不一樣的signature。






2017年8月6日 星期日

Parallel Java 1 - Task Parallelism

Async and Finish primitives

這是用high level眼光來看parallelism。

SUM = SUM1 + SUM2

我們說SUM1 和 SUM2 run asynchronizely ,也就是獨立運作,可能in parallel (在不同的CPU core) or not都有可能。

當SUM1 和 SUM2都完成computation,我們說finish,所以finish是定義所有async tasks的scope,而SUM需要等finish才能得到正確答案。

在Java中,async可以用fork()來實現。
finish可以用join()來實現。

例如以下:



或是可以把用parallelize的objects都放入invoke():


這樣會自動fork / join。


Modelling Parallelism

我們可以用graph來model parallelism,假設我們有以下的async/finish execution:


我們可以用以下的graph來model:


directed path意味著sequential dependency,所以沒有被path連接的node tasks,就是可以run in parallel,以上圖來說就是S2 S3。


Work and Span

要衡量computation graph的瓶頸,我們可以引入work / span衡量。
work: 所有node task的cost總和
span: 某條最長path的work


以上圖來說,work = 1 + 10 + 10 + 1 = 22
其中有兩條path的work 都是最大的,是12,所以span = 12

span可以說是瓶頸。
所以如果把 graph work / span稱為"ideal parallelism",我們可以得出一個"多少parallelism"的metric,因為如果能夠parallelized tasks越多,則span越少,那parallelism越大。

ideal parallelism是這個computation graph的parallelism upper bound。


Execution time estimation on multi-core processors

假設有一個parallel computation graph如下:


如果在2-cord processor上面,依照OS的scheduler的演算法,有可能造成不同的分配給CPU的結果。定義Tn = 在n-core上需要執行的cost:


注意T1 = work,因為單一cpu壹定要完成所有的task。
T_inf  = span = 最大的path work,因為其他的tasks都可以跟最大path tasks run in parallel,這使得span成為瓶頸。

所以   T_inf <= Tp <= T1
我們定義 speedup = T1/Tp ,這很好理解。

speedup一定 <= p,因為speedup = p是最理想狀態,發生在work被平均分配給p個processor的graph,則speedup = T1/ (T1/p) = p。不過實際上通常會有一個longest path的出現。

另外speedup <= ideal parallelism,因為之前說過ideal parallelism是一個computation graph parallelism upper bound。

Amdahl's law

這定律很好證明,如果知道一個parallel program中sequential part佔了q %,則speedup <=  1/q。例如假設q = 50%,則直覺來想如果你的program平行化之後,只有50%能run in parallel,那當然最多只能獲得比現在執行時間少一半的效能,也就是 1/0.5 = 2。

這個定律在我們不知道computation graph時來預估speedup有用,證明如下:

1. span >= q * work,這容易理解,因為span就是最大的 path cost,當然 >= 那q percentage的不能被parallelize的部分的work (?? 這解釋有點疑問)

2. 又我們知道 speedup <= work/span這個upper bound,把1. 中的span帶入:
speedup <= 1/q

得證。




2017年8月3日 星期四

Scala Parallel Programming課後心得

上完了!很短的課程。


課程內容我覺得稍微有點失望,主要是習題設計的問題。
習題多數時候比較是在implement application logic,但不見得是parallel construct的運用,所以其實我還是有一點一知半解。

比較好的習題方式應該是多個小的習題,但是每個習題都是給予一個sequential mini project,要我們怎麼利用lecture中交的來改成parallel program,然後比較performance,這樣就很容易懂和進步,因為畢竟之後真的要運用在工作中的話,一定就是找尋可以parallelize的sequential parts,去做performance tuning。

第四個禮拜教了某個parallel data structure的implementation,這有好有壞,不過可惜的是本課程老師口音有點難理解,字幕又是自動翻譯的,常常不正確,造成我很難做筆記,最後就放棄不做了,剛好作業其實跟parallelization的實作相關性不大,變成也不需要太理解lecture video中的內容,也是能過關。

不是很推薦本課程,不過如果想要拿到specialization,就只好拿囉,硬要給分數的話,會給75分。


2017年8月2日 星期三

Scala Parallel Programming筆記 12 - Conc Trees

why Conc Tree?

之前提到,我們採用intermediate data structure來解決"efficient" combining問題。但是另一個可能就是,是否存在本來就是efficient combine operation (concatenate/union)的data structure?


List & Tree

scala list定義如下:(又稱為conc list)


list天生就是sequential,但是tree可以parallel:


我們可以用以下的方法來implement tree parallel filter:


問題是這樣的implementation不能給出一個適合parallelization的balanced tree, 中間的tree不是optimal height因為Leaf case仍會return Empty,如果修改不讓Leaf case return Empty的話,會變成最右邊那樣,像list而非tree,非常不balanced:



Conclist  - balanced tree

為了達成balance,我們要另一個data structure Conc,trait 定義如下:


level :就是height
size: 就是這個tree有幾個elements

以下為Conc trait可能的concrete implementation:



conc tree有invariants必須要遵守:
1. <> (inner node) 不能有Empty child
2. <>的左右children的level差 <= 1,為了要達成balanced tree。以下這個tree就違反了invariant 2:

以下這個才符合variant 2:



所以invariant 2 保證conc tree的operations至少能在O(log n)完成。

我們可以定義一個 <> method,用來當作concatenation (注意這根constructor 名字一樣):


這method裡面先檢查了invariant 1,然後真正做balancing (滿足invariant 2)的是concat method,concat method就檢查invariant 2,如果滿足的話就new 一個inner node <>當成parent node:





否則的話,tree xs和tree ys必須要做combination。
先假設xs的height = d,ys的height = e <= d-2 。
所以要看xs的trees的leaning狀況(哪邊的tree較深)如何來決定要怎麼merge。


1. xs is left-leaning (左邊subtree較深)

注意右邊subtree最多高度d-1 (否則不會通過invariant 2)。則我們就把右邊的subtree與ys concat recursively,最後在形成一個parent node連接兩者:



2. xs is right-leaning,右邊的subtree比較長:

注意下圖把xs的右邊subtree的左右children subtree的可能深度配對都列出來了,分別是:
(d-2, d-2)
(d-3, d-2)
(d-2, d-3)

可能的combination結果如下圖:(沒有完全列出來,看code吧):




重點是 這個combination有達到我們要的O(log(n) + log(m) )嗎?!

注意此演算法的base case是某個兩者高度差 < 某值,所以recursion深度取決於O(xs_height - ys_height) < O(logn),可以說Conc tree是符合parallel combiners需求的data structure。


Conc tree to implement combiner!

Conc tree concatenation兩個tree可以有O(h1-h2),但是我們還需要constant time append operation,才能實現"efficient" parallel combiner,所謂efficient就是 O(logm + logn),n = combiner1 #elements, m = combiner2 #elements。

combiner trait的append就是 += method,假設我們用conc tree來當作combiner的instance variable xs:


我們知道至少要O(log n) time來做append,但是有沒有辦法constant time?

事實上是有ㄟ!
不過要loose Conc tree invariance 2: 仿造 <> node,定義一個Append node:


但是不限定append node的左右children的高度差!所以以下的subtree 是合法的:


所以append很快,可以是O(1),因為只要把append node的children pointers指向兩者就好:


所以Append Node事實上也是intermediate data structure,因為不能parallel execution(因為tree unbalanced),所以勢必也要轉換成balanced tree。

不過這個轉換本身至少需要O(n),所以以上的implementation不適合達成"efficient" concatenation (至少O(logn))!因為在concatenation過程中,變成linked list,而linked list只能linear time traversal。


所以要達成O(log n) concatenation,另一個方法是 限制append node的數量,使得整體的concatenation effort能被O(log n) bounded。

這邊用到二進位的想法:二進位的數字n,但是所需要的bit數目卻是log(n),所以數字線性成長但是bit數目log成長。

以下過程省略,課程字幕實在太爛了,老師口音又常常聽不清楚,放棄!

怎麼用conc tree implement Combiner

這個combiner叫做ConcBuffer,定義如下:


他有一個conc tree memeber,也有一個array,以及chunkSize來記錄第一個empty array entry index。這個index就用來append element,直到array滿了以後,就expand一個新的array,並且把此滿了的array  包成Chunk node放入 conc tree。



Chunk node定義如下,功能類似Conc tree中的SingleNode:


以下又省略了,實在聽得很痛苦,直接寫作業吧!
這門課的口音和字幕帶給我很大痛苦!


2017年7月31日 星期一

Scala Parallel Programming筆記 11 - Splitter and Combiners

Iterator 

首現我們知道Java有iterator:


Splitter (iterator parallel counterpart)


每一個parallel collection都依定會implement Splitter trait。
一但呼叫了split,就分成幾個disjoint splitter set:


原來的splitter instance會變成undefined state。
remaining是"estimate"目前splitter中有幾個element。

來看怎麼用splitter trait implement fold?


首先迴響 for .. yield會產生一個collection,所以children是一個collection for task[Splitter],每個task會recursively呼叫fold,由於每次children都是remaining > threshold時 split出child Splitters而來,所以remaining會越來越少,直到抵達base case去做sequential foldLeft。最後一行還是要記得去呼叫foldLeft在這個children collection上,才能真正去merge。

Builder



result被呼叫的話,就會return collection,而此builder就undefined。

所以builder其實是把一個collection的“增加member"這件事抽象化的trait,

Combiner (parallel Builder)



combiner要efficient需要經過一番努力,所謂的efficient是要在O(logn + logm)時間內完成,當然落,要不然不等於linearly跑過一次,那哪叫什麼efficient combiner。簡單以四核心cpu為例:

一個reduction tree的leaves會分配給4 core cpus,假設剛好分成四份,每份1/4 N,但是在往root combine的過程中,其實總共累積了(粗略計算) 7/4 N > N的工作量,這比單一cpu sequentially map的時間還久!!!

combine對Map/Set來說,是union。
對sequence來說 (List/Vector/Array) combine是concatenation。

可惜的是,對常見的data  structure來說,combine的動作不可能達到efficient (O(logm + logn)。


兩階段parallel construction

但是實際上幾乎所有的scala collection都能轉換成parallel collection,為何?
因為採取了兩階段的collection construction,使用暫時的data structure來儲存state,例如Array 可能採用其他的intermediate data structure來表現,使得efficient combiner可以實現。

這個intermediate data structure有以下的性質:


第三點就是builder/combiner為什麼會有result method的原因,因為要捨棄這個intermediate data structure,convert到真正的data structure,需要再O(n/P) time內,n = size of the data structure, P = # processors。這個convert必須也要能parallellizable。


phase 1:每個processor先呼叫 += 來build intermediate data structures,然後每個ids被combine直到reduction tree root。

phase 2: parallelly build final data structure from ids。

總共約N/2 for 4 processors。

Array Combiners

intermediate data structure主要就是nested array:


先來看怎麼implement += :


這是O(1)。如果裏層array滿了,就new一個新的更大的。注意上圖中其他的第一層array element都是空的。

combine就很簡單了,因為第一層array是一堆Array pointer,所以只要指向另一個被combine的nested array就可:


這邊 ++= 倒是第一次看到的operator! 這是constant time operation 。

再來是conversion result:


後來的筆記都消失了....因為chrome給我當機 @@
火大ㄟ