Μετάβαση στο περιεχόμενο

Εισαγωγή στον παράλληλο προγραμματισμό

Παράλληλος προγραμματισμός

Η πιο συνηθισμένη τακτική προγραμματισμού που συναντάται σε καθημερινή βάση είναι ο σειριακός προγραμματισμός. Σε αυτήν την περίπτωση το πρόγραμμα εκτελείται σειριακά, δηλάδη η μια εντολή μετά την άλλη. Σε κάποιες περιπτώσεις όμως αυτό δεν είναι αρκετό διότι ο περιορισμός στο χρόνο ή στους υπολογιστικούς πόρους μπορεί να είναι απαγορευτικός για τους στόχους μας. Έτσι, δημιουργείται η ανάγκη για μια αλλη τακτική που ακούει στο όνομα παράλληλος προγραμματισμός. Με αυτόν τον τρόπο μπορούμε να εκμεταλλευτούμε την ύπαρξη πολλαπλών επεξεργαστών και να αυξήσουμε την υπολογιστική επίδοση μειώνοντας ταυτόχρονα το χρόνο εκτέλεσης.

Βασικό θέμα στον προγραμματισμό πολλαπλών πυρήνων είναι ο διαμοιρασμός της εργασίας και ο συγχρονισμός μεταξύ των πυρήνων. Υπάρχουν δύο βασικά μοντέλα παράλληλου προγραμματισμού τα οποία μας ενδιαφέρουν:

  1. Message-Passing Programming (MPI)

  2. Shared-Memory Programming (OpenMP)

Τα παράλληλα συστήματα διακρίνονται στα κοινής μνήμης και στα κατανεμημένης μνήμης. Στα πρώτα οι επεξεργαστές έχουν πρόσβαση σε μια κοινή μνήμη, δηλαδή σε όλα τα δεδομένα. Στα δεύτερα ο κάθε επεξεργαστής έχει πρόσβαση στη δική του μνήμη, δηλαδή σε συγκεκριμένα δεδομένα.

Περιγραφή του MPI

Το MPI είναι μια βιβλιοθήκη/διεπαφή μεταβίβασης μηνυμάτων για την επικοινωνία παράλληλων διεργασιών και χρησιμοποιείται κυρίως σε συστήματα κατανεμημένης μνήμης. Με το MPI ο υπολογισμός κατανέμεται σε ένα σύνολο πανομοιότυπων διεργασιών, σειριακά αριθμημένων, που εκτελούνται σε διαφορετικούς κόμβους και ο κώδικας τους διαφοροποιείται ρητά ανάλογα με τον σειριακό αριθμό τους.

MPI εντολές

Επιλογή βιβλιοθήκης MPI

Για να κάνουμε compile ή για να εκτελέσουμε ένα MPI παράλληλο πρόγραμμα πρέπει να ενεργοποιήσουμε κάποια βιβλιοθήκη του MPI (π.χ. mpich, openmpi, mvapich2, intel-mpi).

Προτείνεται η χρήση μίας έκδοσης mpi που να αξιοποιεί το FDR (InfiniBand) interconnect στα batch και gpu partitions της συστοιχίας όπου και είναι διαθέσιμο, π.χ. ενδεικτικά ένα από τα εξής:

# module load gcc/9.2.0 openmpi/3.1.3
# module load gcc/9.2.0 openmpi/3.1.5  # έκδοση κατάλληλη για χρήση MPI παραλληλίας σε συνδυασμό με CUDA
# module load gcc/9.2.0 openmpi/4.0.3  # έκδοση κατάλληλη για χρήση MPI παραλληλίας σε συνδυασμό με CUDA
# module load gcc/9.2.0 mvapich2/2.3.4
# module load intel/19.0.5 intel-mpi/2019.8.254

Το MPI υποστηρίζεται απο προγράμματα που είναι γραμμένα σε C/C++ και Fortran. Για την κάθε γλώσσα υπάρχει και η αντίστοιχη εντολή για compile και link. Έτσι:

  • mpicc : Κάνει compile και link προγράμματα γραμμένα σε C.
  • mpiCC ή mpic++: Κάνει compile και link προγράμματα γραμμένα σε C++.
  • mpif90 : Κάνει compile και link προγράμματα γραμμένα σε Fortran 90.

    Αν για παράδειγμα θέλουμε να κάνουμε compile το foo.c και να δημιουργήσουμε ένα εκτελέσιμο αρχείο C με όνομα foo θα γράψουμε:

    # mpicc -o foo foo.c     (C)
    

    Αν θέλουμε να κάνουμε compile το foo.cc και να δημιουργήσουμε ένα εκτελέσιμο αρχείο C++ με όνομα foo θα γράψουμε:

    # mpiCC -o foo foo.cc    (C++)
    

    ή

    # mpic++ -o foo foo.cc    (C++)
    

    Αντίστοιχα για ενα αρχείο Fortran 90 με όνομα foo.f θα γράψουμε:

    # mpif90 -o foo foo.f   (Fortran 90)
    

Run

Σε αντίθεση με τα προηγούμενα, το MPI ορίζει μια κοινή εντολή για την εκτέλεση ενος MPI προγράμματος ανεξαρτήτου γλώσσας. Έτσι, αν θέλουμε να τρέξουμε το πρόγραμμα μας μπορούμε να χρησιμοποιήσουμε μια εκ των mpirun και mpiexec. Στις περισσότερες περιπτώσεις το αποτέλεσμα είναι το ίδιο και με τις δύο εντολές. Η διαφορά έγκειται στο ότι η mpiexec ορίστηκε εξ αρχής στο MPI standard ενω η mpirun προστέθηκε αργότερα σε διάφορες εκδοχές του MPI (Mpich, OpenMPI κλπ). Για να είμαστε σίγουροι για τη συμπεριφορά της εντολής προτιμούμε συνήθως την mpiexec.

Η σύνταξη της εντολής είναι η ακόλουθη:

mpiexec  [options] <program> [<args>]

Το program είναι το όνομα του εκτελέσιμου αρχείου, το args είναι προαιρετικοί παράμετροι που αφορούν το εκτελέσιμο ενω το options είναι παράμετροι που αφορούν την εντολή mpirun και μπορούν να πάρουν συγκεκριμένες τιμές:

  • Χρησιμοποιώντας το -np μπορούμε να ορίσουμε τον αριθμό των επεξεργαστών που θέλουμε. Προφανώς η τιμή πρέπει να είναι θετικός ακέραιος. Για παράδειγμα, αν θέλουμε να τρέξουμε το αρχείο με όνομα a.out και χρειαζόμαστε 8 επεξεργαστές, γράφουμε :
    mpiexec -np 8 ./a.out
    

Σε περίπτωση που θέλουμε να τρέξουμε πολλά αρχεία, μπορούμε να το κάνουμε σε μια γραμμή διαχωρίζοντας το κάθε αρχείο με : . Αν για παράδειγμα θέλουμε να τρέξουμε τα αρχεία ocean και air με 4 και 8 επεξεργαστές αντίστοιχα, γράφουμε:

mpiexec -np 4 ocean : -np 8 air

Σε περίπτωση που θέλουμε να τρέξουμε μια σειριακή εργασία, το μόνο που έχουμε να κάνουμε είναι να θέσουμε τον αριθμό των επεξεργαστών στο 1. Για παράδειγμα:

mpiexec -np 1 ocean

Περιγραφή του OpenMP

Το OpenMP είναι ένα περιβάλλον προγραμματισμού που αποτελείται απο εντολές, συναρτήσεις και μεταβλητές και χρησιμοποιείται κυρίως σε συστήματα κοινής μνήμης. Μπορεί να θεωρηθεί ως μια επέκταση στις γλώσσες C/C++ και Fortran. Με τις κατάλληλες εντολές τα ιδιαίτερα "βαριά" σημεία ενος προγράμματος μπορούν να εκτελεστούν παράλληλα απο πολλούς πυρήνες εφόσον αυτοί υπάρχουν. Επειδή το μοντέλο αυτό χρησιμοποιείται σε συστήματα κοινής μνήμης, εξ ορισμού απαιτεί μόνο ενα κόμβο καθώς είναι αναγκαίο οι διαθέσιμοι επεξεργαστές να έχουν πρόσβαση στην ίδια μνήμη.

OpenMP εντολές

Σε αντίθεση με το MPI, το OpenMP είναι πιο απλό και εύκολο στη χρήση. Δεν χρησιμοποιείται κάποια ειδική εντολή (compile wrapper) όπως η mpicc. Εν αντιθέσει, μπορούμε να χρησιμοποιήσουμε κάποιον GNU, Intel ή PGI compiler.

Για GNU compiler ισχύει:

  • gcc: Κάνει compile προγράμματα γραμμένα σε C. Η σύνταξη είναι:

    gcc <πρόγραμμα> –o <εκτελέσιμο> –fopenmp (C)
    
  • g++: Κάνει compile προγράμματα γραμμένα σε C++. Η σύνταξη είναι:

    g++ <πρόγραμμα> –o <εκτελέσιμο> –fopenmp (C++)
    
  • gfortran: Κάνει compile προγράμματα γραμμένα σε Fortran. Η σύνταξη είναι:

    gfortran <πρόγραμμα> –o <εκτελέσιμο> –fopenmp   (Fortran)
    

Το -fopenmp είναι ενα απαραίτητο flag το οποίο πρέπει να δώσουμε στην εντολή για να καταλάβει το OpenMP κομμάτι του προγράμματος. Για παράδειγμα, αν θέλουμε να κάνουμε compile ενα αρχείο C με όνομα foo.c θα γράψουμε:

    gcc foo.c -o foo -fopenmp

Για Intel compiler ισχύει:

  • icc: Κάνει compile προγράμματα γραμμένα είτε σε C είτε σε C++. Η σύνταξη είναι:

    icc <πρόγραμμα> -o <εκτελέσιμο> -qopenmp (C/C++)
    
  • ifort: Κάνει compile προγράμματα γραμμένα σε Fortran. Η σύνταξη είναι:

    ifort <πρόγραμμα> -o <εκτελέσιμο> -qopenmp (Fortran)
    

Το -qopenmp είναι το αντίστοιχο flag με το -fopenmp αλλα για intel compilers. Αν για παράδειγμα θέλουμε να κάνουμε compile ενα αρχείο C με όνομα foo.c θα γράψουμε:

    icc foo.c -o foo -qopenmp

Για PGI compiler ισχύει:

  • pgcc: Κάνει compile προγράμματα γραμμένα σε C. Η σύνταξη είναι:

    pgcc <πρόγραμμα> –o <εκτελέσιμο> –mp (C)
    
  • pgCC: Κάνει compile προγράμματα γραμμένα σε C++. Η σύνταξη είναι:

    pgCC <πρόγραμμα> –o <εκτελέσιμο> –mp (C++)
    
  • pgf90: Κάνει compile προγράμματα γραμμένα σε Fortran. Η σύνταξη είναι:

    pf90 <πρόγραμμα> –o <εκτελέσιμο> –mp   (Fortran 90)
    

Το -mp είναι το αντίστοιχο flag με το -fopenmp αλλα για PGI compilers. Αν για παράδειγμα θέλουμε να κάνουμε compile ενα αρχείο C με όνομα foo.c θα γράψουμε:

    pgcc foo.c -o foo -mp

Run

Για να τρέξουμε ενα script, πρέπει πρώτα να ορίσουμε τον αριθμό των νημάτων (threads) τα οποία θέλουμε. Αυτό το κάνουμε μέσω της εντολής:

export OMP_NUM_THREADS=<integer>