5. Bố cục đề tài
3.1.2. Song song hóa thuật toán Ford Bellman
Trước tiên ta hãy đi phân tích thuật toán Ford Bellman 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 Ford Bellman tuần tự (xem ở mục 2.2.3.2) Phần khởi tạo chia làm hai phần :
o Khởi tạo ma trận trọng số của đồ thị: Nếu u,v là hai đỉnh bất kỳ của đồ thì mà không có cạnh nối trực tiếp thì gán (u,v) bằng vô cực.
for v V do
for u V do
Nếu a[v,u]== 0 thì a[v,u] =+
o Khởi tạo nhãn d[v] và pre[v] ( với mọi v V). Gọi S là đỉnh xuất phát:
for v V do Begin d[v]= a[S,v]; Pre[v]= S; End Phần tối ưu :
Trong khi còn có thể tối ưu Với mỗi đỉnh u
Với mỗi đỉnh v
Nếu d[v] > d[u] + a[u,v] Begin
d[v] = d[u] + a[u,v]; pre[v]=u ;
Để song song hóa thuật toán Ford Bellman trước tiên ta chia thuật toán thành hai phần để dễ dàng song song hóa:
o Song Song hóa phần khởi tạo Khởi tạo ma trận trọng số
- Quan sát phần khởi tạo ma trận trọng số phía trên ta thấy gán giá trị vô cực cho các cạnh nối giữa hai đỉnh u, v (với mọi u,v thuộc V) của đồ thì nếu hai đỉnh đó không nối trực tiếp với nhau (hay nói cách khác cạnh (u,v) không thuộc E). Giả sử như các cặp đỉnh dưới đây không có cạnh nối trực tiếp: u=0, v=0: a[0,0]= + u=0, v=1: a[0,1]= + u=1, v=0: a[1,0]= + u=1, v=1: a[1,1]= + u=n-1, v=n-1: a[n-1, n-1]= +
- Ta thấy không có sự phụ thuộc dữ liệu giữa các a[u,v] (với mọi u,v thuộc V) với nhau, do khi tính a[0,1] không dùng a[0,0] để tính a[0,1], cứ tương tự như vậy, a[1,0] cũng không dùng a[0,0] hay a[0,1] để tính a[1,0],… - Code CUDA song song cho phần khởi tạo ma trận trọng số
__global__ void KernelKhoiTaoMatran(int *a, size_t n) {
int tidX = blockIdx.x * blockDim.x + threadIdx.x; int tidY = blockIdx.y * blockDim.y + threadIdx.y; if (tidX < n && tidY < n)
{
int index = tidY * n + 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 (hai vòng lặp for dùng để duyệt ta 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 v (với mọi v thuộc V), tidY đóng vai trò như đỉnh u (với mọi u thuộc V). Còn index là tính chỉ số vị trí trong ma trận a[u,v] (hay a[tidY, tidX]).
Khởi tạo nhãn d[v] và pre[v]
- Quan sát phần khởi tạo nhãn d[v] và pre[v] phía trên, với vòng lặp for chạy từ 0 đến n-1 : v=0: d[0]= a[S,0]; pre[0]= S; v=1: d[1]= a[S,1]; pre[1]= S; v=2: d[2]= a[S,2]; pre[2]= S; v=3: d[3]= a[S,3]; pre[3]= S; v=n-1: d[n-1]= a[S,n-1]; pre[n-1]= S;
- Ta thấy không có sự phụ thuộc dữ liệu nào, vì khi tính d[1] ta không dùng lại d[0] (và ma trận a không thay đổi gì), cả mảng lưu vết pre cũng thế chỉ gán một giá trị là đỉnh xuất phát S, cũng không bị phụ thuộc dữ liệu. Không có sự dùng lại dữ liệu bước trước để tính bước sau (tính nhãn d và pre), việc tính là độc lập nhau.
- Code CUDA song song cho phần khởi tạo nhãn
__global__ void KernelKhoiTaoNhan(int *a, int *d, int *pre, size_t S, size_t n)
{
int tidX = blockIdx.x * blockDim.x + threadIdx.x; if (tidX < n) { d[tidX]= a[S*n+tidX]; pre[tidX]=S; } }
o Song Song hóa phần tối ưu
Song song hóa phần tối ưu theo một chiều (1D) - Ta hãy xem vòng lặp for thứ ba
Với mỗi đỉnh v
Nếu d[v] > d[u] + a[u,v] Begin
d[v] = d[u] + a[u,v]; pre[v]= u ;
End
- Ta hãy hãy mô tả chúng như sau (giả sử thỏa điều kiện d[v]>d[u]+a[u,v]) v=0: d[0]= d[u] + a[u,0]; pre[0]= u
v=1: d[1]= d[u] + a[u,1]; pre[1]= u v=2: d[2]= d[u] + a[u,2]; pre[2]= u v=3: d[3]= d[u] + a[u,3]; pre[3]= u
v=n-1: d[n-1]= d[u] + a[u, n-1]; pre[n-1]= u
- Không có sự dùng lại dữ liệu của d[v] bước trước để tính d[v] bước sau, pre[v] cũng vậy. Do đó ta có thể song song hóa được phần này.
- Code CUDA song song cho phần tối ưu một chiều (1D)
__global__ void kernelToiUu1D(int *a,int *d,int *pre,size_t n,size_t u) {
int tidX = blockIdx.x * blockDim.x + threadIdx.x; if (tidX < n)
{ int index= u*n+ tidX;
if(d[tidX] > (d[u]+a[index])) { d[tidX]= d[u]+a[index]; pre [tidX]= u; } }
}
Song song phần tối ưu theo 2 chiều (2D) - Ta hãy xem đoạn code tuần tự dưới đây:
Với mỗi đỉnh u
Với mỗi đỉnh v
Nếu d[v] > d[u] + a[u,v] Begin
d[v] = d[u] + a[u,v]; pre[v]= u ;
End
- Để 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 d[v] > d[u] + a[u,v] và cập nhật d[v] = d[u] + a[u,v]), 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 *d,int *pre,size_t n) {
int tidX = blockIdx.x * blockDim.x + threadIdx.x; int tidY = blockIdx.y * blockDim.y + threadIdx.y;
if (tidX < n && tidY < n)
{ int index= tidY*n+ tidX;
if(d[tidX] > (d[tidY]+a[index])) {
d[tidX]= d[tidY] + a[index]; pre[tidX]= tidY;
} } }
- Khi thực hiện đoạn code này kết quả chạy đúng (so với kết quả chạy code For Bellman tuần tự và chạy bằng 1 chiều (1D)), vậy ta có thể song song hóa được hai vòng lặp for phía trên.