上完了!很短的課程。
課程內容我覺得稍微有點失望,主要是習題設計的問題。
習題多數時候比較是在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分。
code
2017年8月3日 星期四
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 memeber,也有一個array,以及chunkSize來記錄第一個empty array entry index。這個index就用來append element,直到array滿了以後,就expand一個新的array,並且把此滿了的array 包成Chunk node放入 conc tree。
Chunk node定義如下,功能類似Conc tree中的SingleNode:
以下又省略了,實在聽得很痛苦,直接寫作業吧!
這門課的口音和字幕帶給我很大痛苦!
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 )。
訂閱:
文章 (Atom)

















































