code

顯示具有 Scala 標籤的文章。 顯示所有文章
顯示具有 Scala 標籤的文章。 顯示所有文章

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給我當機 @@
火大ㄟ

Scala Parallel Programming筆記 10 - Scala parallel collections

Sequential Collection Hierarchy



Parallel Collection Hierarchy


注意中間的generic collection系列是用在當collection未知是sequential or parallel的時候。

舉例來說,如果要找某個array中最大的palindrome:


由於xs是一個GenSeq[Int],所以只要是這個trait的subclass都能不管是否真的parallel或是sequential而去呼叫以下:



.par conversion time

把一個collection轉換成parallel collection是需要時間的,一個100萬element的list需要128 ms,而一個100萬elements的vector只需要0.0015 ms,這是由於list並非parallelizable collection,所以scala把list轉換成最接近形式的parallel collection: vector。

以下是parallel collections:


所以挑選一個parallel collection要很注意!不要選錯。


小心使用thread-unsafe collection

以下是一個錯誤的範例:


這邊有個讓我發現我觀念錯誤的地方就是,其實不是把0 until 1000給拆成總共加起來是1000個的數份collection,而是語意會達成sequential的語意,也就是應該會拆成{parallel set a} x {parallel set b} 的組合(但是這樣應該會重複計算? 不過parallelism會彌補),要不然帶入setA和setB都是disjoint parallel set的話,就變成{parallel set a} x {parallel set b} 組合的其中幾種,而且結果會是不確定的。所以不是我想的那樣,但是這個寫法還是錯!

錯誤在於mutable.Set是not thread-safe,所以每次+=都會update同一個object(同時也驗證上面講的{parallel set a} x {parallel set b} ),所以會錯。

其中一種解決方法是改用thread-safe collection取代mutable.Set:


另一個方法是使用combinator,避免mutation side effects:


這邊所謂的combinator是指"filter" ,丟入b(_),課堂中講這是似乎有Int=>Boolean的signature,所以是一個function object,代表把set b整個丟入去filter。
其實光是a.filter(b(_))即可,但是如果要efficient,就可以用比較小的set去當filter parameter。

不要在mutable collection traverse時 update / read

這個就算在sequential case也成立。
請看以下錯誤範例:


這邊錯誤(1)在traverse graph.par時,graph(k)被寫入graph(v),我們不能在traverse的過程中還update。(2) graph(v) 這是一個read operation,但是collection卻正在被(1) update!因為mutable.Map不是一個thread-safe collection!!!!

TrieMap collection

上面的問題可以用一個concurrent TrieMap collection來解決:因為TrieMap提供了atomic snapshot功能,能把某個state確切snapshot起來(在collection.par開始時),所以之後的traverse都是用這個snapshot,而之後的update都不會被看見。


所以這邊有三個graph instance:
1. 在graph.par (被convert成parallel collection)時,一個紅色的snapshot被“隱性自動的”建立了,此snapshot在traverse過程中都一直被用到,所以graph(k) = previous(v)不會影響此紅色snapshot

2. 藍色的instance就是這個TrieMap本身,是mutable,真正的update在這個collection

3. 粉容色的instance是一個我們自己建立的snapshot,這是用來保存update之前的狀態,因為此program有需要用到。

這樣有了1,我們就不會在traverse時候遇到內容被update會打亂traverse state的問題
有了3,我們就不會遇到去讀正在update的graph裡面的某個value,可能state被打亂的問題。



Scala Parallel Programming筆記 9 - Data Parallel Operations

fold construct

scala collection多數能直接轉成parallel collection,透過.par method:


所以filter / count 都可以execute in parallel。例如我們可以寫一個collection sum:

def sum(xs:Array[Int]):Int = {
  xs.par.foldLeft(0)((x,y) => x+y)
}

可惜foldLeft不能直接parallelize,先看其signature:


所以foldLeft accumulator type B,collection element type A,透過f(B,A) => B來把每一個element fold成一個單一值。但是每次進入f的B其實是前一次f的output,也就是foldLeft只能sequential computation。

fold/reduce/scan 都是只能sequential operation。

如果fold是以下的signature的話,就可以用parallelization (reduction tree):


事實上scala有fold function,是專門給parallel computation用的。當然要用parallel fold還是得要遵守operator必須是associative的前提,舉例來說,以下的operator (play剪刀石頭布)只有communtative,但是不associative,所以不同的execution order會有不同的結果:


所以初始值(z)和binary operator f必須滿足以下性質(稱為monoid關係):


所以z丟進去f其實就是a的identity function。

可是fold很明顯不夠用,因為所有參數都只能是same type,這只能用在受限的application context。

Aggregation construct

解決fold不夠expressive的問題,另一個parallel operation稱為aggregation:


比較特別的是,他接受兩個operators f and g,f是sequential operator, g是parallel operator,為什麼要這樣?

注意f是跟foldLeft的operator f一樣的signature,而g的signature是跟fold的operator一樣的signature,所以aggregation事實上是combine foldLeft 和 fold:


所以把array 先分成不同parts,然後每個parts用sequential foldLeft,最後用fold combine,不過問題是,為什麼這樣可以?

舉以下的例子:


的確這是可行的,因為sequential的f不依賴任何其他parts results,而parallel的g符合monoid structure ( 0 + count = count 且 + operator是associative )。