5. Bố cục đề tài
3.1.3. Song song hóa thuật toán Floyd
Trước tiên ta hãy đi phân tích thuật toán Floyd tuần tự có thể song song hóa được những phần nào (trình bày ở mục 1.1.4 và mục 1.1.5). Từ đó ta áp dụng phương pháp trình bày ở mục 1.1.6 để thực hiện việc song song.
Thuật toán Floyd tuần tự (xem ở mục 2.2.4)
Khởi tạo giá trị đầu cho hai ma trận a và pre
Ma trận pre lưu vết đường đi, chương trình chưa bắt đầu nên giá trị của pre được gán tất cả các giá trị pre[i, j] = j.
Giá trị của ma trận a chính là độ dài ngắn nhất giữa mọi cặp đỉnh, khởi đầu giá trị của a chính là độ dài thực trên đồ thị G(V,E). Chính là độ dài thực tế giữa hai đỉnh i và j. for i=0 to n-1 do for j= 0 to n-1 do { pre[i,j]= j; if(a[i,j] <= 0) a[i,j]=+ }
Tối ưu: Xác định độ dài đường đi ngắn nhất từ i đến j trên đồ thị và lưu vết đường đi qua n lần lặp (xem mục 2.2.4.)
for k=0 to n-1 do for i=0 to n-1 do for j=0 to n-1 do
if a[i, j] > a[i, k] + a [k, j] then Begin
pre[i, j]=pre[i,k]; End
Để song song hóa thuật toán Floyd trước tiên ta chia thuật toán Floyd thành hai phần để dễ dàng song song hóa:
o Song song hóa phần khởi tạo
- Quan sát đoạn mô tả dưới đây mô tả việc khởi tạo giá trị đầu cho hai ma trận a và pre. Giả sử đối với việc gán ma trận a thỏa điều kiện a[i,j] <= 0
i=0 o j=0: pre[0,0]= 0; a[0,0]= + ∞ o j=1: pre[0,1]= 1; a[0,1]= + ∞ o j=2: pre[0,2]= 2; a[0,2]= + ∞ o j=n-1: pre[0, n-1]= n-1; a[0, n-1]= + ∞ i=1 o j=0: pre[1,0]= 0; a[1,0]= + ∞ o j=1: pre[1,1]= 1; a[1,1]= + ∞ o j=2: pre[1,2]= 2; a[1,2]= + ∞ o j=n-1: pre[1, n-1]= n-1; a[1, n-1]= + ∞ i=2 o j=0: pre[2,0]= 0; a[2,0]= + ∞ o j=1: pre[2,1]= 1; a[2,1]= + ∞ o j=2: pre[2,2]= 2; a[2,2]= + ∞ o j=n-1: pre[2, n-1]= n-1; a[2, n-1]= + ∞
- Ta thấy không có sự phụ thuộc dữ liệu giữa các giá trị của ma trận a do không dùng lại dữ liệu của ma trận a bước trước đó để tính cho giá trị bước sau, tương tự ma trận pre cũng vậy việc tính toán là độc lập dữ liệu.
- Code CUDA song song cho phần khởi tạo ma trận trọng số
__global__ void kernelInit(int *pre, size_t n) {
int index;
int tidX = blockIdx.x * blockDim.x + threadIdx.x; int tidY = blockIdx.y * blockDim.y + threadIdx.y; if (tidX < n && tidY < n) {
index = tidY * n + tidX; pre[index]= tidX; if(a[index]<=0)
a[index]=VOCUC; } }
- Giải thích: Ta thấy không còn hai vòng lặp for nữa (2 vòng lặp for dùng để duyệt qua các đỉnh, để xét tất cả các cạnh nối giữa các đỉnh), còn ở code CUDA này tidX đóng vai trò như đỉnh j (với mọi j thuộc V), tidY đóng vai trò như đỉnh i (với mọi i thuộc V). Còn index là tính chỉ số vị trí trong ma trận a[i,j] (hay a[tidY, tidX]).
o Song song hóa phần tối ưu - Song song hóa phần tối ưu
Song song hóa phần tối ưu theo một chiều (1D) o Ta hãy xem vòng lặp for thứ ba
for j=0 to n do
if a[i, j] > a[i, k] + a[k, j] then {
a[i, j]= a[i, k] + a[k, j]; pre[i, j]=pre[i,k]; }
o Ta hãy mô tả đoạn code trên như sau (giả sử thỏa điều kiện a[i,j] > a[i, k] + a[k, j] )
j= 0: a[i, 0]= a[i, k] + a[k,0]; pre[i, 0]= pre[i, k] j= 1: a[i, 1]= a[i, k] + a[k,1]; pre[i, 1]= pre[i, k] j= 2: a[i, 2]= a[i, k] + a[k, 2]; pre[i, 2]= pre[i, k]
j= 3: a[i, 3]= a[i, k] + a[k, 3]; pre[i, 3]= pre[i, k]
j= n-1: a[i, n-1]= a[i,k] + a[k, n-1]; pre[i, n-1]= pre[i, k]
o Ta thấy không có sự phụ thuộc dữ liệu, do không dùng lại dữ liệu ở bước trước để tính bước sau. Ví dụ như để tính a[i,1] ta không dùng kết quả của a[i,0] được tính ở bước j=0 để tính a[i,1] ở bước j=1. o Code CUDA cho phần tối ưu một chiều
__global__ void kernelToiUu(int *a,int *pre, size_t n, size_t k,size_t i) {
int tidX = blockIdx.x * blockDim.x + threadIdx.x; if (tidX < n)
{
int indexkj = k * n + tidX; int indexij = i * n + tidX;
if(a[indexij]> a[i*n+k]+ a[indexkj] ) {
a[indexij]= a[i*n+k]+ a[indexkj]; pre[indexij] = pre[i*n+k];
} } }
Song song phần tối ưu theo 2 chiều (2D) o Ta hãy xem hai vòng lặp for sau
for i=0 to n-1 do
for j=0 to n-1 do
if a[i, j] > a[i, k] + a [k, j] then Begin
a[i, j]= a[i, k] + a[k, j]; pre[i, j]= pre[i, k]; End
o Để biết được có song song hóa được hai vòng lặp for này, nhóm tác giả đã dùng phương pháp thực nghiệm để chứng minh (do hai vòng lặp for này khó chứng minh bằng lý thuyết vì ta cần phải đưa ra một ma trận đồ thị tốt để có thể mô tả được các trường hợp so sánh a[i,j] > a[i, k] + a[k, j] và cập nhật a[i, j]= a[i, k] + a[k, j], tượng tự đối với ma trận pre), nếu kết quả chạy đúng thì xem như song song hóa được hai vòng lặp for này, ngược lại nếu kết quả chạy sai thì không song song hóa được.
__global__ void kernelToiUu(int *a,int *pre, size_t n, size_t k) {
int indexkj, indexij;
int tidX = blockIdx.x * blockDim.x + threadIdx.x; // vai trò là j int tidY = blockIdx.y * blockDim.y + threadIdx.y; // vai trò là i if (tidX < n && tidY < n)
{
indexkj = k * n + tidX;
indexij = tidY * n + tidX;
if(a[tidY*n+k]+ a[indexkj]) < a[indexij])
{
a[indexij]=a[tidY*n+k]+ a[indexkj];
pre[indexij] = pre[tidY*n+k];
}
} }
o Khi thực hiện đoạn code này thì kết quả chạy đúng (so với chạy bằng tuần tự và chạy bằng 1 chiều (1D)), như vậy ta có thể song song hóa được hai vòng lặp for này.