四月八、九號兩日,學生有幸受命(也不算拉XD)參加了「台灣多核心計算學會籌備處」所舉辦的開發課程,課程過程中學生記錄了兩天的重點筆記以及課後的一些實作實驗,台灣目前在Xeon Phi開發的人數較少,參考資料也不多,希望可以盡一點微薄之力幫助同好,也吸引同好前往討論。

第一天內容大致上是著墨於Intel MIC Architecture的簡介,隨後介紹了Xeon Phi的基本架構以及運作原理,並且比較了他和原本的Ivy Bridge CPU的差異,最後講了Xeon Phi的Programming Model以及優化重點,詳細內容將在本為內有細部的「筆記」供參考。

而第二天則將重心放在Intel的開發環境與工具(icc與icpc Compiler、TBB、Cilk+、MKL、VTune Amplifier XE),其實直接一點來說也可以說他在推銷Intel Composer XE也不為過的(Composer XE 2013包含了所有他介紹的供功能XD),在介紹完大部分的工具之後,花了約一小時的時間來講解程式碼最佳化的步驟,講師用了五個步驟來進行這件事,也算是兩天課程以來,讓我印象較深刻的部份。

Intel MIC Architecture 

Intel MIC全名為Intel Many Integrated Core ,顧名思義其實就是很多的core被整合的意思,Intel Xeon Phi就是基於這種架構下所開發出來的產品,以實驗室的7120p為例,他擁有61個cores,每個core擁有4個threads,他擁有CPU擁有的特性(擁有自己的cache、共享卡上面的memory等等),並且擁有SIMD指令集的支援(在 Intel MIC Architecture之下有一個專用的SIMD指令集叫做IMCI),由這樣的特性所開發出來的產品,能做的事情比起NVIDIA CUDA以量取勝的threads來說,算是有不同的市場區別考量。

Xeon Phi可以運行所有原本已經寫好的C/C++/Fortran程式碼(當然需要重新compile),但其實效能並不會優於Host CPU,原因在於Host CPU的時脈高出Xeon Phi的時脈太多了(與E5 – 2620比較,Turbo boost之後,時脈分別為2.5GHz與1.33GHz的差異),若想要再Xeon Phi上面擁有更好的效能,則需要做「向量化」與「平行化」兩個步驟來大幅提昇其運算效能。

Intel Programming Model

與GPU較不同的地方,GPU一定要將Code從Host Offload到GPU進行運算,而Xeon Phi可以執行「Native Mode」與「Oflload Mode」,後者就與GPU相同,可以將指定區塊的程式碼Offload到卡上面進行運算,而前者「Native Mode」則是直接讓程式碼運行在卡上面,不需透過CPU來進行分配工作的工作。

Xeon Phi的Offload Mode運算方式與GPU很相似,透過五個步驟來進行,分別是:Allocate->Copy Over->Read/Modify->Copy Back->Free五個步驟。

此外,由於Xeon Phi透過網路做連結溝通,若需要使用多張卡進行MPI運算也是可行的,必須透過NFS來進行資料交換與溝通,NFS的設置也可以幫助資料從Host傳送到MIC Card上,讓自己減少一些繁雜的步驟。

 

Intel IMCI (Initial Many-Core Instructions)

在Xeon Phi擁有一個特色,就是他擁有512bit wide的register可以一次計算16個單精度浮點數(16*32bit),舉例來說:

for(int i=0; i<n ;i++)
	A[i] += B[i];

這是一個沒有使用IMCI指令集的程式碼,我們希望將A陣列加上B陣列的值存回A陣列,若是沒有使用SIMD指令集的話,則一個長度為n的陣列就必須要做n次計算。

for (int i=0; i<n; i+=16) {
	__m512 Avec=_mm512_load_ps(A+i); 
	__m512 Bvec=_mm512_load_ps(B+i); 
	Avec=_mm512_add_ps(Avec, Bvec); 
	_mm512_store_ps(A+i, Avec);
}

但若是使用IMCI指令集來做這件事情,在Intel Xeon Phi上面則可以一次運算16個浮點數運算,將迴圈運行的次數從n次變成n/16次,大幅提昇了單一核心的運算效能。

在這個部分,其實牽涉到SSE與AVX指令集的運用,我在參與這場Workshop之前完全沒有用過這樣的Intrinsics,所以有很多部分寫的不是很完整,有機會可以更加熟悉Intrinsics的運用會在補上其他說明。

 

向量化與平行化 (Vectorization and Parallelization)

所謂向量化,其實就是讓CPU或Coprocessor可以進行SIMD的運算,在參與這場Workshop之前,我還真以為SIMD就是「單一指令多個資料」的處理,以為使用OpenMP把資料分給很多個Core算就是SIMD運算,這下子可發現我大錯特錯了,SIMD強大的地方在於可以在同樣的cycle內做更多的運算,這是學生念大學以來都不知道的事情阿!(慚愧)

那要如何做向量化呢?方法如下:

  1. Intrinsics – 上一個段落就是透過Intrinsics來進行向量化
  2. Compiler Auto Vectorization – 在Intel的Compiler當中,預設就會將程式碼做自動向量化(難怪icc的效能這麼好),使用者並不需要做什麼特別的設定,當然限制也會有點多,不過可以簡單的換Compiler的動作提昇大幅度效能
  3. Array notation – 在Cilk+當中支援的一種array表示方式,他也可以透過icc來進行自動向量化
  4. Other – 其他的像是OpenMP 4.0所支援的SIMD指令等等

在以上這些條件之下,Intel 的icc與icpc Compiler都會做最佳化,替這些程式碼進行向量化,提昇運算效能,但也由於方法的不同,提昇的效能也不同,最後的範例當中將會提到我自己所做的實驗數據差異。

 

而平行化,就是我一開始所誤會SIMD的意思了,就是把工作分給多個實體threads去做,來直接提昇效能。

平行化的方法如下:

  1. OpenMP – OpenMP提供較簡單的語法來提供工作分配,尤其在於for迴圈的平行部分
  2. Cilk+ – 講師表示,Intel在做市場定位時,也希望把Cilk+當做類似OpenMP無痛升級的一種library,讓使用者可以簡單的來做平行化,其實Cilk的for迴圈也相當簡單好用,最後的範例也會舉例,實際上比較,Cilk+在Xeon Phi上面運行的速度也高於OpenMP的哦
  3. TBB – Threading Building Block的簡稱,也是由Intel所提供的符合STL函式庫,提供非常多的Generic Algorithms Pattern Template供使用者直接使用,適合大量使用C++的人使用

在前陣子我做過一個實驗來比較OpenMP與TBB的效能,TBB的效能遠高於OpenMP的平行,所以讓我一心想要往TBB去學習,結果上了課才發現,其實Cilk+也可以用簡單的方式來達的很好的效能…只能說相見恨晚阿!

結合了向量化與平行化之後,我們就能直接用Xeon Phi,透過VPU在一個cycle的時間計算 61*16=976 個浮點數運算拉*!這也是Intel Xeon Phi與NVIDIA CUDA較為不同的地方,但也因為支援SIMD才讓Xeon Phi可以只擁有61個cores就能匹敵CUDA囉!

這個部分較特別的地方是,不管是在Xeon Phi或是Xeon平台上,都適用這樣的Optimized method,所以在優化Xeon Phi程式時,確實可以嘗試從Host版本一路改成Xeon Phi的版本。

 

*註解:我不是很確定Xeon Phi每個Core裡面所擁有的VPU是一個Core只擁有一個,還是4個Threads就可以用4個,但也由於Hyper Thread實際效能並不是4倍,所以就算是每個Thread都可以用自己的VPU,效能也不會是四倍囉

Step by step optimized

較詳細的工具運用也比較沒辦法做太多的筆記說明,只能從架構上來理解為何需要做怎麼樣的修正來達到更好的效能,這其實也是原本來參加這場workshop最大的用意,不過順便聽聽一些自己從來都不知道的工具也不錯啦,anyway,題外話。

 

講師在第二天的最後一個多小時,示範了所謂的「Step by step optimized」,其實這段演講蠻精采的,可以直接看見怎麼樣的調教會得到怎麼樣的優化,那step by step的步驟呢?

  1. Using optimized tools, library
  2. scalar, serials optimized
  3. Vectorization
  4. Parallelization
  5. Multi-core to Many-core

首先,第一步驟的意思是說,若我們可以直接替換Intel的Compiler,那效能就會直接提昇,(想起當天有個學員問講師一個有關效能問題,講師跟他說直接換Intel Compiler就是了)或是使用Intel提供已經存在且可以直接使用的MKL、TBB等High Performance Librarys。

第二個步驟,透過VTune來進行Hot sopt分析。(這個部分我還沒有玩過,所以沒辦法示範囉,有機會再補上)

第三個步驟,透過Auto Vectorization或Intrinsics來進行向量化、SIMD,其實第一個步驟當中,當我們使用Intel Compiler時就已經替我們自動向量化,想要擁有更好的效能的話,必須透過Intrinsics來撰寫SIMD指令。

#include "cstdlib"
#include "iostream"
#include "mkl.h"

using namespace std;
using std::cout;
 
int main(){
	const int n=1073741824;
	const int x=10; //test times
	int i,j;
	int A[n];
	int B[n];

	cout << "Warm up.." << endl;
	// 陣列初始化
	for (i=0; i<n; i++)
		A[i]=B[i]=i;

	cout << "done!" << endl;

	double aveTime,minTime=1e6,maxTime=0.;

	//計算十次的平均時間
	for (j=0; j<x ; j++)
	{
		cout << "Performing a[i]=a[i]+b[i] " << j << endl;
	 
		double startTime = dsecnd();
		// 當使用Intel Compiler時,這個迴圈會被自動向量化
		for (i=0; i<n; i++)
			A[i]+=B[i];
		
   		double endTime = dsecnd();
   		double runtime = endTime-startTime;
	 
		maxTime=(maxTime > runtime)?maxTime:runtime;
		minTime=(minTime < runtime)?minTime:runtime;
		aveTime += runtime;
 	}
	aveTime /= x;
 
 	cout << "maxRT:       " << maxTime << endl;
 	cout << "minRT:       " << minTime << endl;
 	cout << "aveRT:       " << aveTime << endl;
	
	//印出最後的值確認是否正確
 	cout << "check value A[1073741823]:       " << A[1073741823] << endl;
 	cout << "check value B[1073741823]:       " << B[1073741823] << endl;
	
	
}
$ icpc -mkl -O0  step1.cpp -o step1_O0
$ time ./step1_O0 

Performing a[i]=a[i]+b[i] 8
Performing a[i]=a[i]+b[i] 9
maxRT:       4.75663
minRT:       4.75555
aveRT:       4.75622


$ icpc -mkl step1.cpp -o step1
$ time ./step1

Performing a[i]=a[i]+b[i] 8
Performing a[i]=a[i]+b[i] 9
maxRT:       1.52117
minRT:       1.51973
aveRT:       1.52064

 

由於前三個步驟較容易一次Demo完畢,所以我把它合成同一個Code,透過icpc Compiler用不一樣的參數(-O0)來驗證自動平行化的好處,第一個code block是step1~3的Code,第二個code block是執行結果的部份擷取,RT表示Run Time,單位為秒。

第四個步驟,透過Cilk+或是OpenMP來進行多核分工,也就是平行化運算。

//計算十次的平均時間
	
		// 當使用Intel Compiler時,這個迴圈會被自動向量化
		// 並且透過OpenMP的Parallel for來平行化這個迴圈
#pragma omp parallel for 
		for (i=0; i<n; i++)
			A[i]+=B[i];
		
$ icpc -mkl -openmp step4.cpp -o step4
$ ./step4 

Performing a[i]=a[i]+b[i] 8
Performing a[i]=a[i]+b[i] 9
maxRT:       1.62199
minRT:       0.871249
aveRT:       0.94813

以上為OpenMP的平行方式,只需要找到對應的for loop並在前面加上#pragma omp parallel for即可,從數據可以得值,平均時間是比單純使用Auto Vetroiztion有縮短的。

 

第五個步驟,從Intel Xeon平台移到Intel Xeon Phi平台上跑吧!

$ icpc -mkl -mmic -O3 sample1.cpp -o sample1
$ ./sample1

Performing a[i]=a[i]+b[i] 8
Performing a[i]=a[i]+b[i] 9
maxRT:       0.230543
minRT:       0.0772056
aveRT:       0.0978042

我用了不同的檔名以區分程式的不同,其實程式碼與上面的Step4是相同的,只需要使用不同的Compile參數即可,可以看得出來,跑出來的時間勝過CPU很多。

 

Summary

之前總是單純的使用OpenMP來進行平行化,實際上提昇的效能大約只有兩倍(平行化率100%的程式碼),在Workshop當中知道原來有「向量化」這回事,算是這兩天課程的一大收穫!

整份比較若有任何錯誤,請不吝指教,也別忘了分享您的意見讓台灣HPC的風氣可以逐漸上升哦!

 

Reference

Book

Parallel Programming and Optimization with Intel Xeon Phi Coprocessors, Colfax 2013 http://www.colfax-intl.com/nd/xeonphi/book.aspx .

Website

Best Practice Guide – Intel Xeon Phi, http://www.prace-project.eu/Best-Practice-Guide-Intel-Xeon-Phi-HTML

Intel Developer Zone: Intel Xeon Phi Coprocessor, http://software.intel.com/en-us/mic-developer .

Building a Native Application for Intel Xeon Phi Coprocessors, http://software.intel.com/en-us/articles/building-a-native-application-for-intel-xeon-phi-coprocessors .

 

大部分的參考內容與程式碼改寫都是從Parallel Programming and Optimization with Intel Xeon Phi Coprocessors當中所研讀並實作出來的內容,第二個網址則提供了一些有用的參考,也算是初步起步時不錯的參考,另外兩個內容是來自於Intel官方,就不冗述囉:P