Mô hình truyền thông điệp thƣờng đƣợc dùng với kiến trúc bộ nhớ phân tán. Mô hình này có những đặc điểm sau đây:
Các tác vụ tính toán sử dụng các vùng nhớ cục bộ của chúng trong quá trình thực thi. Các tác vụ này có thể đƣợc thực thi trên cùng một máy tính hoặc (phổ biến hơn) trên nhiều máy tính.
Các tác vụ này trao đổi dữ liệu bằng cách gửi và nhận thông điệp.
Việc truyền dữ liệu giữa các tác vụ cần phải đồng bộ hóa giữa tác vụ gửi và tác vụ nhận, theo nghĩa, khi có một tác vụ truyền thông điệp thì phải có một tác vụ khác nhận thông điệp đó.
53
Hình 42 – Mô hình truyền thông điệp
Nguồn: [4]
4. 1. 4. Mô hình song song dữ liệu
Mô hình song song dữ liệu làm việc tốt với cả kiến trúc bộ nhớ chia sẻ lẫn kiến trúc bộ nhớ phân tán. Mô hình này có những đặc điểm sau đây:
Hầu hết các công việc cần xử lý song song đều tập trung vào việc thực thi các tác vụ trên tập hợp dữ liệu. Nói chung tập hợp dữ liệu này đƣợc tổ chức theo một cấu trúc phổ biến, chẳng hạn một dãy các dữ liệu.
Các tác vụ tính toán cùng làm việc trên một cấu trúc dữ liệu, nhƣng mỗi tác vụ làm việc trên một phần riêng biệt của cấu trúc dữ liệu đó.
Các tác vụ thực thi các công việc giống nhau trên những phần dữ liệu riêng của chúng, ví dụ "Cộng thêm 4 vào tất cả các phần tử".
54
Hình 43 – Mô hình song song dữ liệu
Nguồn: [4]
4. 2. Các ƣu điểm của mô hình truyền thông điệp
4. 2. 1. Phổ biến
Mô hình truyền thông điệp rất phù hợp với hệ thống gồm các bộ xử lý riêng rẽ đƣợc kết nối với nhau thành một mạng cục bộ. Vì thế mô hình này phù hợp một cách tự nhiên với các siêu máy tính song song đang thịnh hành.
4. 2. 2. Khả năng mô tả
Mô hình truyền thông điệp đƣợc xem nhƣ một mô hình hoàn chỉnh để diễn tả các thuật toán xử lý song song. Mô hình này cung cấp khả năng quản lý tính cục bộ dữ liệu – lƣu ý rằng đây chính là thiếu sót trong mô hình song song dữ liệu. Mô hình truyền thông điệp rất phù hợp với các thuật toán tự trị (Adaptive Algorithm, loại thuật toán có khả năng thay đổi tính chất tùy vào tài nguyên đƣợc cung cấp, chẳng hạn khi có thêm bộ nhớ thì độ phức tạp thuật toán giảm xuống [31]), hoặc các chƣơng trình có khả năng tiếp tục hoạt động ngay cả khi có sự không cân bằng về tốc độ tính toán [9].
55
4. 2. 3. Dễ gỡ lỗi hơn
Bẫy lỗi và gỡ lỗi cho các chƣơng trình xử lý song song hiện nay vẫn là một lĩnh vực nghiên cứu đầy thách thức. Nói chung việc viết các bộ bẫy / gỡ lỗi cho các chƣơng trình xử lý song song là dễ dàng hơn đối với mô hình chia sẻ bộ nhớ. Tuy nhiên, có lý do để nói rằng quá trình bẫy / gỡ lỗi diễn ra dễ dàng hơn trong mô hình truyền thông điệp. Đó là vì một trong những nguyên nhân phổ biến nhất dẫn đến lỗi trong các chƣơng trình xử lý song song là developer vô tình ghi đè lên vùng nhớ đã tồn tại. Mô hình truyền thông điệp quản lý việc truy cập bộ nhớ chặt chẽ hơn so với các mô hình khác (chỉ có một tiến trình duy nhất đƣợc phép truy cập trực tiếp tới một vùng nhớ cụ thể) nên tạo điều kiện để developer dễ dàng tìm ra những thao tác đọc / ghi gây lỗi. Một số trình gỡ lỗi thậm chí còn có thể hiển thị hàng đợi các thông điệp, điều mà các developer thƣờng không thể biết đƣợc.
4. 2. 4. Hiệu năng
Nguyên nhân cơ bản nhất khiến cho truyền thông điệp là mô hình không thể thiếu trong môi trƣờng tính toán song song chính là hiệu năng. Các CPU hiện đại càng ngày càng nhanh hơn, do đó việc quản lý bộ nhớ cache của chúng (và bộ nhớ nói chung) đã trở thành chìa khóa để tận dụng đƣợc khả năng của phần cứng. Mô hình truyền thông điệp cung cấp một phƣơng tiện để các developer liên kết các dữ liệu cụ thể với các tiến trình, và do đó, cho phép bộ biên dịch và bộ quản lý bộ nhớ cache hoạt động ở hiệu suất tối đa. Lƣu ý rằng trong thực tế, một trong những nguyên nhân khiến hệ thống các máy tính riêng rẽ có thể vƣợt qua khả năng tính toán của những cỗ máy chỉ có một vi xử lý chính là vì chúng có dung lƣợng bộ nhớ chính và bộ nhớ cache lớn hơn. Các ứng dụng phụ thuộc vào tài nguyên nhớ (Memory Bound Application, [31]) có thể đƣợc tăng tốc nếu đƣợc viết lại cho các hệ thống kiểu này. Thậm chí đối với các hệ thống máy tính dùng chung bộ nhớ, việc sử dụng mô hình truyền thông điệp cũng có thể giúp tăng hiệu năng vì developer có khả năng quản lý tính cục bộ dữ liệu.
4. 3. Giới thiệu về MPI
MPI (Message Passing Interface) không phải là một cuộc cách mạng trong lập trình cho các máy tính song song. MPI là tập hợp của những tính năng tốt nhất của rất nhiều hệ
56
thống truyền thông điệp đã tồn tại trƣớc nó. Các tính năng này, khi đƣợc đƣa vào MPI, sẽ đƣợc chuẩn hóa và nếu nhƣ phù hợp thì sẽ đƣợc phát triển thêm.
MPI là một bản mô tả (specification), không phải một bản cài đặt (implementation). Tính tới thời điểm năm 1999 (chƣa rõ con số thống kê cho thời điểm hiện tại), tất cả các nhà cung cấp các hệ thống máy tính song song đều cung cấp các bản cài đặt chuyên biệt hóa của MPI. Ngoài ra, nhiều bản cài đặt MPI miễn phí có thể đƣợc download từ nhiều nguồn trên internet. Do là một bản mô tả không phụ thuộc vào một cài đặt cụ thể nào, nên một chƣơng trình MPI đúng nghĩa phải có khả năng chạy trên tất cả các bản cài đặt mà không cần thay đổi gì. MPI có các phiên bản dành cho các ngôn ngữ khác nhau nhƣ C, C++, Fortran.
MPI là một thƣ viện, không phải một ngôn ngữ. Nó định nghĩa tên, các chuỗi lời gọi và kết quả của các tiến trình con đƣợc gọi từ một chƣơng trình Fortran; các hàm sẽ đƣợc gọi từ một chƣơng trình C; và các lớp và phƣơng thức tạo nên thƣ viện MPI cho C++. Các chƣơng trình MPI đƣợc viết bằng Fortran, C hoặc C++ đƣợc biên dịch bằng công cụ biên dịch thông thƣờng của các ngôn ngữ này và đƣợc liên kết tới thƣ viện MPI tƣơng ứng.
4. 4. Mục tiêu của MPI
Mục tiêu chính của MPI là để giúp các developer không cần thỏa hiệp giữa tính hiệu quả (efficiency), linh động (portability) và khả năng hoạt động (functionality). Cụ thể hơn, ngƣời dùng có thể viết các chƣơng trình có tính linh động cao trong khi vẫn tận dụng đƣợc khả năng từ các phần cứng và phần mềm đƣợc chuyên biệt hóa từ những nhà cung cấp khác nhau. Lƣu ý rằng một lớp ngƣời dùng quan trọng của MPI chính là những nhà phát triển các thƣ viện xử lý song song, đối với lớp ngƣời dùng này thì nhu cầu về tính hiệu quả, linh động và khả năng hoạt động là rất lớn.
4. 5. Các đặc điểm của MPI
MPI hỗ trợ việc lập trình song song: MPI là hệ thống cho phép các máy con trong cụm có thể xử lý song song và giải quyết chung một vấn đề. Nếu một cluster bao gồm 1000 máy con, thì một chƣơng trình MPI lý tƣởng sẽ cho phép tăng tốc tính toán lên gấp 1000 lần. Tuy nhiên, tốc độ tính toán thực sự thì phụ thuộc vào nhiều yếu tố, bao gồm
57
loại hình vấn đề đang phải giải quyết, đặc điểm của thuật toán sử dụng, và kỹ năng của ngƣời lập trình.
MPI làm việc trên hệ thống bộ nhớ phân tán: Để tăng tính hiệu quả, MPI thƣờng chạy trên các cluster gồm các máy tính cùng loại, sử dụng các kết nối mạng tốc độ cao. Tuy nhiên, MPI sử dụng một hệ thống bộ nhớ phân tán, theo nghĩa mỗi máy con trong cluster có một bộ nhớ riêng rẽ, dữ liệu cần thiết cho tác vụ đang giải quyết đƣợc chia ra cho tất cả các máy trong cluster. Điều này có nghĩa là trƣớc hoặc trong quá trình xử lý, một máy con trong cluster có thể yêu cầu các dữ liệu hoặc kết quả tính toán từ các máy khác trong cluster.
MPI là một giao diện lập trình, không phải một hệ thống đã cài đặt sẵn: Các bản cài đặt MPI có thể đƣợc viết bằng nhiều ngôn ngữ, nhƣ C, C++, Fortran, .. Việc biên dịch một chƣơng trình MPI giống hệt nhƣ biên dịch một chƣơng trình thông thƣờng. Chẳng hạn, nếu chƣơng trình đƣợc viết bằng C, thì ngƣời lập trình chỉ đơn giản include thƣ viện MPI và biên dịch sử dụng gcc.
Các máy con trong cluster thực thi cùng một chƣơng trình: Ngƣời lập trình viết một chƣơng trình duy nhất. Tại thời điểm biên dịch, bản thực thi của chƣơng trình đƣợc tạo và tồn tại duy nhất tại máy biên dịch. Thông thƣờng, khi ngƣời quản trị yêu cầu thực thi một chƣơng trình MPI, bản thực thi này sẽ đƣợc tự động copy đến tất cả các máy con đƣợc ngƣời quản trị yêu cầu tham gia tính toán, và sau đó bắt đầu thực thi đồng thời trên các máy này. Tuy nhiên, đối với các chƣơng trình đƣợc thực thi nhiều lần, hoặc để giảm thời gian không cần thiết, việc copy bản thực thi đến các máy con trong cụm có thể đƣợc tiến hành thủ công bởi ngƣời quản trị. Do mỗi máy con trong cụm đƣợc phân biệt bởi một số nhận dạng tƣơng ứng và duy nhất, nên mỗi máy có thể xử lý một công việc khác nhau, mặc dù chúng cùng thực thi một chƣơng trình. Theo thông lệ, máy con với mã số 0 (nhƣ ví dụ dƣới đây) sẽ chịu trách nhiệm thu thập kết quả tính toán từ các máy con khác và xử lý tiếp.
Các đoạn mã có sẵn cần phải đƣợc chuyển đổi phù hợp để có thể sử dụng MPI hiệu quả: Ngƣời lập trình cần nghĩ đến một phƣơng án để có thể chia nhỏ bài toán hiện tại thành các bài toán con có độ phức tạp tƣơng đƣơng nhau sao cho các máy tham gia tính toán có thể hoàn thành đƣợc trong khoảng thời gian phù hợp. Nói cách khác, ngƣời lập trình phải "tƣ duy lại theo kiểu lập trình song song".
58
4. 6. Khác biệt giữa các bản cài đặt bằng C và C++ của MPI
Một khác biệt quan trọng trong cách cài đặt MPI sử dụng C và C++ nằm ở cách xử lý các giá trị trả về. Trong C, giá trị trả về thực sự đƣợc truyền nhƣ tham số (ví dụ, tham số size trong MPI_Comm_size thực ra là biến lƣu kết quả), còn giá trị trả về thông thƣờng thực chất lại là mã lỗi. Nếu mã lỗi trả về không phải là MPI_SUCCESS, thì mặc định tất cả các tiến trình sẽ dừng thực thi. Nếu các tiến trình vẫn tiếp tục thực thi, thì giá trị trả về của các hàm khi đó sẽ là mã lỗi tƣơng ứng. Trong C++, giá trị trả về thông thƣờng chính là giá trị trả về thực sự (ví dụ, MPI::Comm::Get_size()). Khi có lỗi xảy ra, mặc định tất cả các chƣơng trình cũng sẽ dừng thực thi giống nhƣ C, nhƣng trong trƣờng hợp các tiến trình vẫn tiếp tục thực thi, thì thay vì trả về mã lỗi, chƣơng trình sẽ ném ra một exception.
Dƣới đây là bảng tƣơng ứng các hàm MPI trong C và C++.
Bảng 1 – Tương ứng các hàm MPI trong C và C++
C C++
int MPI_Init(int *argc, char ***argv) void MPI::Init(int& argc, char**& argv) void MPI::Init( )
int MPI_Comm_size(MPI_Comm comm, int *size)
int MPI::Comm::Get_size( ) const int MPI_Comm_rank(MPI_Comm comm, int
*rank)
int MPI::Comm::Get_rank( ) const int MPI_Bcast(void *buf, int count,
MPI_Datatype datatype, int root, MPI_Comm comm)
void MPI::Intracomm::Bcast(void* buffer, int count, const Datatype& datatype, int root) const
int MPI_Reduce(void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)
void MPI::Intracomm::Reduce(const void* sendbuf, void* recvbuf, int count, const Datatype& datatype, const Op& op, int root) const
int MPI_Finalize( ) void MPI::Finalize( )
Lƣu ý rằng, từ đây trở đi, chúng ta chỉ xét thƣ viện MPI đƣợc cài đặt bằng C.
4. 7. MPI trên Rocks
59
Hình 44 – MPI trên Rocks
Khi một ứng dụng MPI đƣợc gọi, các ứng dụng MPI trên các máy compute đƣợc sử dụng trong quá trình tính toán sẽ lần lƣợt đƣợc gọi thông qua SSH. Đây chính là một lý do khiến mỗi ngƣời dùng đều phải tạo cặp khóa private – public ngay lần đầu khởi động terminal trên máy frontend.
4. 8. Viết chƣơng trình Hello World
Dƣới đây là mã nguồn chƣơng trình viết bằng C. #include "mpi.h"
#include <stdio.h>
int main(int argc, char* argv[]) { int processId; int noProcesses; int nameSize; char computerName[MPI_MAX_PROCESSOR_NAME]; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &noProcesses); MPI_Comm_rank(MPI_COMM_WORLD, &processId); MPI_Get_processor_name(computerName, &nameSize);
60
fprintf(stderr, "Hello from process %d on %s\n", processId, computerName);
MPI_Finalize(); return 0;
}
Lƣu mã nguồn trên thành file hello.c, và biên dịch bằng mpicc. Command biên dịch là:
mpicc -o hello hello.c
Dƣới đây là kết quả khi chạy với các tham số khác nhau. Chạy trên local (máy frontend)
Từ thƣ mục chứa file hello (output của quá trình biên dịch bên trên), thực thi lệnh mpirun -np 5 hello
61
Hình 45 – Output của chương trình Hello World (1)
Mặc định, khi ngƣời quản trị không chỉ định sẽ sử dụng máy nào trong cluster để thực thi chƣơng trình, thì chƣơng trình sẽ thực thi ngay trên local (máy frontend). Tham số np cho biết số tiến trình sẽ đƣợc khởi động. Do ta xác định số tiến trình là 5, nhƣng lại chỉ có một máy thực thi (máy local) nên các tiến trình sẽ chia sẻ thời gian CPU (trong trƣờng hợp máy thực thi có đủ 5 bộ xử lý thì mỗi tiến trình sẽ chạy trên một bộ xử lý).
Chạy trên các máy compute
Ta tạo mới một file là machines trên cùng thƣ mục với file hello, với nội dung là: compute-0-0
compute-0-1
Khởi động các máy compute-0-0 và compute-0-1 trong cluster, sau đó thực thi các lệnh sau để copy file hello đến tất cả các máy này:
62
scp ~/hello root@compute-0-1:~
Thực thi bằng lệnh:
mpirun -np 2 -machinefile machines hello
Khi đó output sẽ là:
Hình 46 – Output của chương trình Hello World (2)
Nếu sửa lại file machines thành localhost
compute-0-1
63
Hình 47 – Output của chương trình Hello World (3)
Nếu sửa file machines thành: compute-0-1
64
Hình 48 – Output của chương trình Hello World (4)
Ở trƣờng hợp đầu, số lƣợng tiến trình bằng số lƣợng máy con tham gia thực thi (thực chất là bằng số lƣợng bộ xử lý tham gia tính toán, nhƣng vì mỗi máy có một bộ xử lý nên ta dùng khái niệm "máy con" thay cho khái niệm "bộ xử lý"), nên mỗi máy sẽ thực thi một tiến trình.
Ở các trƣờng hợp sau, do số lƣợng tiến trình nhiều hơn số lƣợng máy con nên sẽ có máy phải xử lý nhiều hơn một tiến trình.
4. 9. Các hàm MPI cơ bản
4. 8. 1. Hàm MPI_Init
Hàm này đƣợc sử dụng để bắt đầu một phiên làm việc MPI. Tất cả các chƣơng trình MPI đều phải gọi đến hàm này nhƣng chỉ gọi duy nhất 1 lần. Nói chung, hàm MPI_Init cần đƣợc gọi trƣớc tất cả các hàm MPI khác (điều đó có nghĩa là nếu trƣớc lời gọi MPI_Init có thêm các lời gọi hàm khác nhƣng các lời gọi hàm này không trực tiếp hoặc
65
gián tiếp gọi đến một hàm MPI_Init thì cũng không sao). Ngoại lệ duy nhất là hàm MPI_Initialized. Hàm này đƣợc sử dụng để kiểm tra xem hàm MPI_Init đã đƣợc gọi đến hay chƣa. Lƣu ý rằng hàm MPI_Init không nhất thiết phải đƣợc gọi trực tiếp trong hàm main: Nó có thể đƣợc gọi gián tiếp thông qua một hoặc nhiều hàm khác. Trong các chƣơng trình C, có thể truyền các tham số địa chỉ argc và argv để có thể tận dụng các tham số đƣợc truyền vào từ dòng lệnh, tuy nhiên điều này không bắt buộc: Các địa chỉ này có thể đƣợc thay thế bởi hằng NULL.
4. 8. 2. Hàm MPI_Finalize
Hàm này đƣợc dùng để đóng một phiên làm việc MPI. MPI_Finalize phải là lời gọi hàm MPI cuối cùng trong chƣơng trình. MPI_Finalize đƣợc sử dụng để giải quyết các phần việc nhƣ giải phóng bộ nhớ, ... Tuy nhiên, ngƣời lập trình cần phải đảm bảo