Παράλληλος Προγραµµατισµός - mmcsl

..
..
..
..
..
Πανεπιστήµιο Πελοποννήσου
Παράλληλος
Προγραµµατισµός
.
.
.
.
.
.
σε C µε χρήση MPI
∆.Σ. Βλάχος και Θ.Η. Σίµος
.
.
.
.
..
..
..
..
..
.
.
.
.
.
.
.
.
.
.
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σκοπός του µαθήµατος
Εισαγωγή
Οι σηµερινοί υπολογιστές είναι περίπου 100 φορές πιο γρήγοροι από αυτούς που
κατασκευάζονταν πριν από 10 χρόνια, παρ’ όλ’ αυτά όµως, υπάρχουν πολλοί
επιστήµονες και µηχανικοί σήµερα που χρειάζονται ακόµη µεγαλύτερες ταχύτητες.
Συχνά χρειάζεται να κάνουν σηµαντικές απλουστεύσεις στα προβλήµατα που έχουν να
λύσουν και ακόµη και έτσι, χρειάζεται να περιµένουν πολλές φορές και εβδοµάδες για να
πάρουν τα αποτελέσµατά τους.
Οι γρήγοροι υπολογιστές είναι το κλειδί για µεγάλους σε όγκο υπολογισµούς. Αν οι
υπολογιστές ξαφνικά τρέχουν 10 φορές πιο γρήγορα, τότε αποτελέσµατα που θα ήταν
εφικτά σε 1 εβδοµάδα θα µπορούν να παραχθούν σε µία νύχτα.
Βέβαια, θα µπορούσε κάποιος να περιµένει 5 χρόνια σύµφωνα µε το νόµο του Moore, για
να έχει αυτήν την εξέλιξη. Από την άλλη όµως, η δυνατότητα αυτή δίνεται σήµερα µε τη
χρήση των παράλληλων υπολογιστών.
Τι είναι οι παράλληλοι υπολογισµοί;
Παράλληλος υπολογισµός είναι η χρήση παράλληλων υπολογιστών για τη
µείωση του χρόνου που απαιτείται για τη λύση ενός και µοναδικού
υπολογιστικού προβλήµατος
Για να µη φανεί ότι αυτό αποτελεί σήµερα µια ερευνητική επιδίωξη της επιστήµης της
πληροφορικής, αναφέρουµε εδώ ότι η χρήση παράλληλων υπολογισµών αποτελεί ένα
δεδοµένο για πολλές εφαρµογές όπως οι αστρονοµικοί υπολογισµοί, η πρόγνωση του
καιρού, η σχεδίαση µε χρήση συστηµάτων CAD, οι υπολογισµοί µοριακών δυναµικών
κ.α.
Τι είναι οι παράλληλοι υπολογιστές;
Ο παράλληλος υπολογιστής είναι ένα υπολογιστικό σύστηµα µε πολλούς
επεξεργαστές που µπορεί να υποστηρίξει παράλληλους υπολογισµούς.
Υπάρχουν δύο κατηγορίες παράλληλων υπολογιστών:
1.
Τα συστήµατα πολλών υπολογιστών που είναι κατασκευασµένα από ένα
αριθµό ανεξάρτητων υπολογιστών που επικοινωνούν µεταξύ τους µε
ανταλλαγή µηνυµάτων και
1
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
2.
Οι συµµετρικοί πολυεπεξεργαστές (Symmetric Multi-Processor, SMP) που
είναι ολοκληρωµένα συστήµατα πολλών επεξεργαστών που
µοιράζονται την ίδια κοινή µνήµη.
Τι είναι ο παράλληλος προγραµµατισµός;
Ο παράλληλος προγραµµατισµός είναι ο προγραµµατισµός σε µία γλώσσα που
επιτρέπει διαφορετικά τµήµατα της υπολογιστικής διαδικασίας να εκτελούνται
ταυτόχρονα σε διαφορετικούς υπολογιστές.
Είναι ο παράλληλος προγραµµατισµός αναγκαίος;
Παρ΄ όλη τη σηµαντική έρευνα που έχει γίνει για την ανάπτυξη µεταφραστών,
ειδικά σε FORTRAN 77 και C, οι οποίοι να µεταφράζουν σε κώδικα
κατάλληλο για παράλληλους υπολογιστές, τα συστήµατα που έχουν αναπτυχθεί
σε καµία περίπτωση δεν φτάνουν τις επιδόσεις που επιτυγχάνονται µε τον
παράλληλο προγραµµατισµό.
Τι είναι το MPI;
Το MPI (Message Passing Interface) είναι ένα σύνολο προδιαγραφών για τις
βιβλιοθήκες που υλοποιούν διαβίβαση µηνυµάτων. Οι βιβλιοθήκες αυτές
είναι διαθέσιµες για οποιοδήποτε τύπο υπολογιστή και λειτουργικού
συστήµατος (παράλληλων και κλασσικών) και µπορούν ακόµα να
χρησιµοποιηθούν για παράλληλα συστήµατα που χτίζονται µε συµβατικούς
υπολογιστές. Η ανάπτυξη προγραµµάτων χρησιµοποιώντας το MPI
διασφαλίζει τη µεταφορά των προγραµµάτων σε διάφορες πλατφόρµες.
Επιστηµονικοί υπολογισµοί
Η κλασσική επιστηµονική µέθοδος βασίζεται στην παρατήρηση, τη θεωρία και τον
πειραµατισµό. Η παρατήρηση οδηγεί σε µια υπόθεση. Οι επιστήµονες αναπτύσσουν
θεωρίες για να εξηγήσουν τα φαινόµενα που παρατηρούν και στη συνέχεια σχεδιάζουν
πειράµατα για να επαληθεύσουν, συµπληρώσουν ή ακόµα και απορρίψουν τις θεωρίες
τους.
Σε αντίθεση µε την κλασσική επιστηµονική µέθοδο, η σύγχρονη επιστηµονική µέθοδος
βασίζεται στην παρατήρηση, τη θεωρία, το πείραµα και την αριθµητική εξοµοίωση. Η
αριθµητική εξοµοίωση έχει γίνει σήµερα ένα από τα πιο σηµαντικά εργαλεία στα χέρια
των επιστηµόνων και πολλές φορές µπορεί και να αντικαταστήσει το πείραµα.
Οι επιστήµονες σήµερα συγκρίνουν τα αποτελέσµατα της αριθµητικής εξοµοίωσης και µε
βάση αυτά εµπλουτίζουν τη θεωρία και σχεδιάζουν νέα πειράµατα. Υπάρχουν πάρα
πολλά επιστηµονικά προβλήµατα που είναι τόσο δύσκολο να εξοµοιωθούν αριθµητικά,
που η χρήση των παράλληλων υπολογιστών είναι αναγκαία. Μερικά από αυτά αφορούν:
•
Κβαντική χηµεία
•
Στατιστική φυσική και σχετικιστική φυσική
2
∆.Σ. Βλάχος και Θ.Η. Σίµος
•
Κοσµολογία και αστροφυσική
Σχήµα 1. Η εισαγωγή της αριθµητικής εξοµοίωσης διαχωρίζει τη σύγχρονη από την
κλασσική επιστηµονική µέθοδο.
•
∆υναµική των ρευστών
•
Σχεδίαση υλικών και υπεραγωγιµότητα
•
Βιολογία και φαρµακολογία
•
Ιατρική και γενετική
•
Μετεωρολογία κ.α.
∆εν είναι δύσκολο να διαπιστώσει κανείς, πως η ανάγκη για πιο αξιόπιστη αριθµητική
εξοµοίωση είναι αυτή που έδωσε καθοριστική ώθηση τα τελευταία χρόνια για ανάπτυξη
των παράλληλων υπολογιστών.
Η εξέλιξη των υπερυπολογιστών
Ο όρος υπερυπολογιστής εµφανίστηκε το 1976 µε την κατασκευή του Gray-1. Ο
Gray-1 ήταν ένας διανυσµατικός επεξεργαστής µε δυνατότητα ταυτόχρονης εκτέλεσης
εντολών (ή µέρους των εντολών). Ο Gray-1 µπορούσε να κάνει 100 εκατοµµύρια πράξεις
κινητής υποδιαστολής σε ένα δευτερόλεπτο. Αρχικά το υψηλό κόστος των
υπερυπολογιστών τους έκανε να είναι διαθέσιµοι µόνο σε κυβερνητικούς οργανισµούς.
Με την πάροδο των χρόνων όµως, πανεπιστήµια και µεγάλες επιχειρήσεις
χρησιµοποίησαν υπερυπολογιστές για ερευνητικούς σκοπούς και για να βελτιώσουν τις
παραγωγικές τους διαδικασίες. Στα µέσα της δεκαετίας του ’80 η χρήση των
υπερυπολογιστών γίνεται πια και από µικρότερες επιχειρήσεις.
3
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ο πρώτος υπερυπολογιστής για την εποχή του, κατασκευάστηκε πριν από 60 περίπου
χρόνια και είχε το όνοµα ENIAC. Ο ENIAC είχε δυνατότητα εκτέλεσης 350 πράξεων
κινητής υποδιαστολής σε ένα δευτερόλεπτο. Σήµερα, ένας προσωπικός υπολογιστής που
βασίζεται σε επεξεργαστή PENTIUM µπορεί να εκτελέσει πράξεις 1 εκατοµµύριο φορές
πιο γρήγορα από τον ENIAC. Η εξέλιξη αυτή βασίζεται κυρίως στην αύξηση της
ταχύτητας του ρολογιού που είναι δυνατή µε την εκπληκτική αύξηση της πυκνότητας
ολοκλήρωσης των ψηφιακών κυκλωµάτων σε ένα και µόνο chip. Οι υπερυπολογιστές
σήµερα µπορούν να εκτελέσουν πράξεις 1 δισεκατοµµύριο φορές πιο γρήγορα από τον
ENIAC. Η µεγάλη διαφορά από τους προσωπικούς υπολογιστές βρίσκεται στη
δυνατότητα παράλληλων υπολογισµών.
Σύγχρονοι παράλληλοι υπολογιστές
Οι παράλληλοι υπολογιστές πήραν τεράστια ώθηση και έγιναν ελκυστικοί για το ευρύ
κοινό µε την ανάπτυξη της τεχνολογίας VLSI. Μερικοί από τους ιστορικούς
παράλληλους υπολογιστές είναι:
Ο Cosmic Cube. Χτίστηκε στο πανεπιστήµιο του Caltech το 1981 και αποτελούνταν
από 64 Intel 8086 (εκείνη την εποχή ο µοναδικός επεξεργαστής που είχε διαθέσιµο
µαθηµατικό συνεπεξεργαστή ήταν ο 8086).
Ο Beowulf. Χτίστηκε στη NASA το 1994 και αποτελούνταν από 16 Intel DX4
συνδεδεµένους µε πολλαπλούς Ethernet 10Mb/sec συνδέσµους. Το σηµαντικό µε τον
Beowulf ήταν ότι αποτελούνταν εξ’ ολοκλήρου από υλικά διαθέσιµα στο εµπόριο και
από λογισµικό που διατίθεται ελεύθερα.
Ο ASCI White. Αποτελείται από 8192 PowerPC και φτάνει σε ταχύτητα 10
τρισεκατοµµυρίων εντολών το δευτερόλεπτο. Το 2000, θεωρούνταν ο πιο γρήγορος
υπολογιστής του κόσµου.
Σχήµα 2. Ο ASCI White αποτελείται από 8192 PowerPC και φτάνει σε ταχύτητα 10
τρισεκατοµµυρίων εντολών το δευτερόλεπτο. Το 2000, θεωρούνταν ο πιο γρήγορος
υπολογιστής του κόσµου.
4
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παραλληλοποίηση
Όπως είδαµε, οι παράλληλοι υπολογιστές είναι σήµερα πιο προσιτοί από κάθε άλλη
εποχή, αλλά για να εκµεταλλευτεί κανείς αυτό το γεγονός πρέπει να µπορεί να διακρίνει
ποιοι υπολογισµοί µπορούν να εκτελεστούν παράλληλα.
Γράφοι εξάρτησης δεδοµένων
Ένας απλός και συνηθισµένος τρόπος για να εξετάσει κανείς ποιες εργασίες µπορούν να
εκτελεστούν παράλληλα, είναι να σχεδιάσει το λεγόµενο γράφο εξάρτησης δεδοµένων
(data dependence graph). Ο γράφος εξάρτησης δεδοµένων είναι ένας κατευθυνόµενος
γράφος στον οποίο κάθε κορυφή αντιστοιχεί σε µια εργασία που πρέπει να εκτελεστεί.
Μια ακµή από την κορυφή u στην κορυφή v σηµαίνει πως η διαδικασία u πρέπει να
ολοκληρωθεί πριν την έναρξη της διαδικασίας v.Τότε λέµε πως η διαδικασία v είναι
εξαρτηµένη από τη διαδικασία u. Αν δεν υπάρχει διαδροµή από τη διαδικασία u στη
διαδικασία v τότε λέµε ότι οι δύο διαδικασίες είναι ανεξάρτητες και µπορούν να
εκτελεστούν παράλληλα.
Υπάρχουν δύο είδη διαδικασιών που µπορούν να εκτελεστούν παράλληλα και µπορούν
εύκολα να διαπιστωθούν σε ένα γράφο εξάρτησης δεδοµένων:
Παραλληλισµός δεδοµένων
Ένας γράφος εξάρτησης δεδοµένων παρουσιάζει παραλληλία δεδοµένων όταν
υπάρχουν διαδικασίες που εφαρµόζουν την ίδια εργασία σε διαφορετικά
δεδοµένα
Για παράδειγµα, αν θέλουµε να προσθέσουµε τα διανύσµατα b[0..99], c[0..99] και το
αποτέλεσµα να αποθηκευτεί στο διάνυσµα a[0..99] ο γράφος εξάρτησης δεδοµένων θα
είναι κάπως έτσι:
...
a0=b0+c0
........
a99=b99+c99
Συναρτησιακός παραλληλισµός
Ένας γράφος εξάρτησης δεδοµένων παρουσιάζει συναρτησιακό παραλληλισµό
όταν διαφορετικές λειτουργίες που εφαρµόζονται σε διαφορετικά δεδοµένα
µπορούν να εκτελεστούν παράλληλα.
Για παράδειγµα θεωρείστε τον ακόλουθο γράφο:
5
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
a=2, b=3
s=(a2+b2)/2
m=(a+b)/2
u=s-m2
Εδώ ο υπολογισµός του m και του s είναι διαφορετικές λειτουργίες και µπορούν να
εκτελεστούν παράλληλα.
Στο παρακάτω σχήµα φαίνονται τα δύο είδη παραλληλισµού και η σειριακή λειτουργία.
Σχήµα 3. Τα δύο είδη παραλληλισµού και η σειριακή λειτουργία όπως φαίνονται σε ένα
γράφο εξάρτησης δεδοµένων. Στο (a) φαίνεται ο παραλληλισµός δεδοµένων όπου η ίδια
διαδικασία Β εφαρµόζεται σε διαφορετικά δεδοµένα. Στο (b) φαίνεται ο συναρτησιακός
παραλληλισµός όπου τρεις διαφορετικές διαδικασίες, οι B,C και D, µπορούν να
εκτελεστούν παράλληλα. Τέλος στο (c) φαίνεται η σειριακή λειτουργία όπου όλες οι
διαδικασίες είναι εξαρτηµένες.
Pipelining
Ένας γράφος εξάρτησης δεδοµένων που σχηµατίζει ένα µονοπάτι δεν επιδέχεται
παραλληλοποίηση αν κάθε λειτουργία που αντιστοιχεί σε κάθε κορυφή γίνεται σε ένα
στάδιο. Από την άλλη όµως, ένας υπολογισµός µπορεί να χωριστεί σε στάδια τα οποία
µπορούν να εκτελεστούν παράλληλα.
6
∆.Σ. Βλάχος και Θ.Η. Σίµος
Φανταστείτε τη γραµµή παραγωγής ενός αυτοκινήτου. Ας υποθέσουµε ότι η παραγωγή
γίνεται σε τέσσερα στάδια και κάθε στάδιο διαρκεί µία ώρα. Αρχικά η γραµµή
παραγωγής είναι άδεια και για να βγει το πρώτο αυτοκίνητο χρειάζεται να περάσουν
τέσσερις ώρες. Όταν τελειώσει το πρώτο στάδιο της παραγωγής του πρώτου αυτοκινήτου
το αυτοκίνητο περνά στο δεύτερο στάδιο και ένα νέο αυτοκίνητο εισέρχεται στο πρώτο
στάδιο παραγωγής. Με αυτόν τον τρόπο το δεύτερο αυτοκίνητο θα βγει σε πέντε αντί σε
οκτώ ώρες, και από εκείνο το σηµείο θα βγαίνει ένα αυτοκίνητο κάθε µία ώρα ( το k
αυτοκίνητο θα βγει σε k+3 ώρες). Η τεχνική αυτή παράλληλης επεξεργασίας ονοµάζεται
pipelining.
Στο σχήµα που ακολουθεί φαίνεται ο γράφος εξάρτησης δεδοµένων για τον υπολογισµό
των µερικώς αθροισµάτων p[0], p[1], p[2], p[3] µε την τεχνική pipelining.
Σχήµα 4. Ένα pipeline για τον υπολογισµό µερικών αθροισµάτων. Κάθε κύκλος
αντιπροσωπεύει µια διαδικασία.
Οµαδοποίηση δεδοµένων
Σαν ένα πρώτο παράδειγµα παραλληλοποίησης ας δούµε ένα πολύ σηµαντικό πρόβληµα
που συναντάται σήµερα στην επιστήµη των υπολογιστών. Ας υποθέσουµε πως έχουµε Ν
κείµενα, κάθε ένα από τα οποία πραγµατεύεται σε κάποιο βαθµό ένα ή περισσότερα από
D θέµατα. Το πρόβληµα είναι να οµαδοποιήσουµε τα κείµενα σε K οµάδες µε τέτοιο
7
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
τρόπο ώστε σε κάθε οµάδα ο βαθµός µε τον οποίο τα κείµενα καλύπτουν τα D θέµατα να
είναι παρόµοιος. Η ποιότητα της οµαδοποίησης θα δίνεται από µια συνάρτηση εκτίµησης
που έχουµε σχεδιάσει (αυτό είναι κάτι που δεν µας ενδιαφέρει αυτή τη στιγµή). Ένας
τυπικός σειριακός αλγόριθµος για τη λύση του προβλήµατος είναι ο ακόλουθος:
1. Εισαγωγή των Ν κειµένων
2. Για κάθε ένα από τα Ν κείµενα, παράγουµε ένα D-διάστατο
διάνυσµα του οποίου κάθε στοιχείο –I αφορά στο βαθµό
κάλυψης του θέµατος –i.
3. Επιλέγουµε αυθαίρετα Κ κέντρα για τις οµάδες µας.
4. Τα επόµενα δύο βήµατα επαναλαµβάνονται ώσπου η συνάρτηση
εκτίµησης συγκλίνει ή έχουµε φτάσει τις Μ επαναλήψεις:
a) Για κάθε ένα από τα Ν κείµενα υπολογίζουµε το
κοντινότερο κέντρο και τοποθετούµε το κείµενο στην
αντίστοιχη οµάδα. Αν το κείµενο αλλάξει οµάδα,
υπολογίζουµε το βαθµό ποιότητας της οµαδοποίησης από τη
συνάρτηση εκτίµησης.
b) Επαναπροσδιορίζουµε τα κέντρα των οµάδων.
5. Έξοδος των Κ οµάδων.
Στο επόµενο σχήµα 5, φαίνεται ο γράφος εξάρτησης δεδοµένων για το παραπάνω
πρόβληµα. Όπως προκύπτει από το γράφο, η παραλληλοποίηση µπορεί να επιτευχθεί στα
παρακάτω σηµεία:
Σχήµα 5. Ο γράφος εξάρτησης δεδοµένων για το πρόβληµα της οµαδοποίησης Ν κειµένων.
8
∆.Σ. Βλάχος και Θ.Η. Σίµος
1.
Κάθε κείµενο µπορεί να εισαχθεί παράλληλα.
2.
Κάθε διάνυσµα κειµένου µπορεί να παραχθεί παράλληλα.
3.
Τα αρχικά κέντρα µπορούν να παραχθούν παράλληλα.
4.
Ο υπολογισµός της κοντινότερης οµάδας για κάθε κείµενο µπορεί να
πραγµατοποιηθεί παράλληλα.
Τα παραπάνω συνιστούν παραλληλοποίηση δεδοµένων. Συναρτησιακή
παραλληλοποίηση συναντάται στην εισαγωγή των κειµένων (µία λειτουργία) και στη
γένεση των κέντρων (δεύτερη λειτουργία).
Προγραµµατίζοντας τους παράλληλους υπολογιστές
Το 1988 οι McGraw και Axelrod θεώρησαν τέσσερις διαφορετικούς τρόπους για την
ανάπτυξη προγραµµάτων σε παράλληλους υπολογιστές:
1.
Ανάπτυξη ενός υπάρχοντος µεταφραστή που θα µεταφράζει σειριακά
προγράµµατα σε παράλληλα προγράµµατα. Είναι η πιο δύσκολη
περίπτωση γιατί πολλές φορές οι δυνατότητες παραλληλισµού είναι
κρυµµένες στο σειριακό κώδικα. Σήµερα υπάρχουν µεταφραστές για τη
FORTRAN 77 που µε τη βοήθεια κάποιων εντολών προς το
µεταφραστή, µπορούν να παράγουν αρκετά αποτελεσµατικούς κώδικες,
σε καµία περίπτωση όµως σαν αυτούς που αναπτύσσει ένας έµπειρος
χρήστης.
2.
Ανάπτυξη µιας υπάρχουσας γλώσσας προγραµµατισµού ώστε να
περιλαµβάνει νέες εντολές µε τις οποίες µπορεί κανείς να περιγράψει
την παραλληλοποίηση. Είναι ο πιο διαδεδοµένος τρόπος για την
ανάπτυξη παράλληλων προγραµµάτων. Το MPI ανήκει σε αυτήν την
κατηγορία.
3.
Ανάπτυξη µιας νέας παράλληλης γλώσσας η οποία θα βασίζεται σε µια
ήδη υπάρχουσα σειριακή γλώσσα. Η διαφορά εδώ είναι ότι η νέα
γλώσσα αποτελείται από δύο επίπεδα. Στο πρώτο γίνεται η κατανοµή
του κώδικα στους διάφορους υπολογιστές που λειτουργούν παράλληλα
και στο δεύτερο η µετάφραση του κώδικα σε κάθε ένα από τους
υπολογιστές αυτούς.
4.
Ανάπτυξη µιας νέας παράλληλης γλώσσας και ενός νέου µεταφραστή.
Η FORTRAN 90 και η C* είναι παραδείγµατα τέτοιων γλωσσών. Το
ερώτηµα εδώ είναι κατά πόσο οι κατασκευαστές του υλικού είναι
διατεθειµένοι να υποστηρίζουν τις νέες αυτές γλώσσες µε την
παραγωγή µεταφραστών για τα µηχανήµατά τους.
9
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
10
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παράλληλες
αρχιτεκτονικές
Εισαγωγή
Από τις αρχές της δεκαετίας του ’60 µέχρι και τα µέσα της δεκαετίας του ’90, οι
επιστήµονες και οι µηχανικοί δοκίµασαν διάφορες αρχιτεκτονικές για παράλληλους
υπολογιστές. Η ανάπτυξη διάφορων τεχνικών έφτασε σε ένα µέγιστο στα µέσα της
δεκαετίας του ’80. Πολλές εταιρίες εκµεταλλεύθηκαν τις τεχνολογικές εξελίξεις της
µικροηλεκτρονικής και ανέπτυξαν δικούς τους επεξεργαστές κατάλληλους για
παράλληλα συστήµατα, ενώ άλλες στηρίχθηκαν σε ήδη υπάρχοντες επεξεργαστές που
χρησιµοποιούνταν σε σταθµούς εργασίας και προσωπικούς υπολογιστές. Οι ειδικοί
διαφωνούσαν µεταξύ τους εάν έπρεπε τα παράλληλα συστήµατα να αποτελούνται από
λίγους ‘γρήγορους’ και ειδικά σχεδιασµένους επεξεργαστές ή από πολλούς και λιγότερο
ισχυρούς.
Σήµερα, λίγο–πολύ, το ερώτηµα αυτό έχει απαντηθεί. Συστήµατα µε ειδικά
σχεδιασµένους επεξεργαστές είναι σπάνια, δεδοµένου ότι αυτοί οι ειδικά σχεδιασµένοι
επεξεργαστές δεν µπορούν σε καµία περίπτωση να ακολουθήσουν την εκπληκτική
εξέλιξη των εµπορικών επεξεργαστών. Αποτέλεσµα αυτού του γεγονότος είναι ότι οι πιο
πολλοί σύγχρονοι παράλληλοι υπολογιστές είναι κατασκευασµένοι από εµπορικά
διαθέσιµους επεξεργαστές.
Καταλαβαίνει λοιπόν κανείς, ότι το πιο σηµαντικό βήµα για την κατασκευή ενός
παράλληλου υπολογιστή δεν είναι τόσο το υλικό που θα χρησιµοποιηθεί αλλά η
τοπολογία του συστήµατος, δηλαδή ο τρόπος µε τον οποίο κατανέµονται στο χώρο οι
επεξεργαστές που απαρτίζουν το σύστηµα αλλά και η µέθοδος που ακολουθείται για να
επικοινωνούν µεταξύ τους. Αναφέρουµε εδώ ότι το Intranet µιας επιχείρησης ή ενός
εκπαιδευτικού οργανισµού µπορεί να µετατραπεί εύκολα σε ένα παράλληλο
υπολογιστικό σύστηµα, µε λογισµικό που υπάρχει και διατίθεται ελεύθερα!
∆ίκτυα διασύνδεσης
Όλα τα συστήµατα µε πολλούς επεξεργαστές πρέπει να έχουν ένα τρόπο µε τον οποίο
αυτοί οι επεξεργαστές θα επικοινωνούν µεταξύ τους. Σε πολλά συστήµατα οι
επεξεργαστές χρησιµοποιούν το δίκτυο διασύνδεσής τους για να προσπελάσουν µια κοινή
µνήµη. Σε άλλα συστήµατα χρησιµοποιούν το δίκτυο διασύνδεσης για να διαβιβάζουν
µηνύµατα ο ένας στον άλλο.
11
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
∆ιανεµηµένα δίκτυα έναντι δικτύων διακοπτών
Οι επεξεργαστές σε ένα παράλληλο υπολογιστικό σύστηµα µπορούν να επικοινωνούν
µεταξύ τους µέσω διανεµηµένων δικτύων ή δικτύων διακοπτών. Στα διανεµηµένα δίκτυα,
οι επεξεργαστές έχουν ένα κοινό διάδροµο επικοινωνίας. Ο κάθε επεξεργαστής µεταδίδει
το µήνυµά του στον κοινό διάδροµο και αυτό το µήνυµα λαµβάνεται από όλους τους
υπόλοιπους επεξεργαστές. Μόνο ο επεξεργαστής στον οποίο απευθύνεται το µήνυµα
ανταποκρίνεται. Ένα γνωστό και πολύ διαδεδοµένο διανεµηµένο δίκτυο είναι το
Ethernet.
Σχήµα 1. ∆ιανεµηµένο (a) δίκτυο και δίκτυο διακοπτών (b).
Σε ένα διανεµηµένο δίκτυο, ο επεξεργαστής που θέλει να στείλει ένα µήνυµα ‘ακούει’ το
διάδροµο. Όταν ο διάδροµος είναι ελεύθερος, στέλνει το µήνυµά του. Αν δύο ή
περισσότεροι επεξεργαστές επιχειρήσουν ταυτόχρονα να στείλουν ένα µήνυµα, τότε αυτά
απορρίπτονται και αναµεταδίδονται. Κάθε επεξεργαστής περιµένει ένα τυχαίο χρονικό
διάστηµα πριν επιχειρήσει να µεταδώσει πάλι το µήνυµά του για να αποφευχθεί εκ νέου
σύγκρουση. Οι συγκρούσεις µηνυµάτων στον κοινό διάδροµο επηρεάζουν αρνητικά την
ταχύτητα επικοινωνίας των επεξεργαστών.
Σε αντίθεση, στα δίκτυα διακοπτών υποστηρίζεται η απ’ ευθείας σύνδεση δύο
επεξεργαστών. Κάθε επεξεργαστής έχει το δικό του διάδροµο µέχρι κάποιον διακόπτη.
Τα δίκτυα διακοπτών έχουν δύο σηµαντικά πλεονεκτήµατα σε σχέση µε τα διανεµηµένα
δίκτυα:
1.
Υποστηρίζουν ταυτόχρονη επικοινωνία µεταξύ πολλών επεξεργαστών
και
2.
Υποστηρίζουν τοπολογίες κλίµακας.
Στη συνέχεια θα ασχοληθούµε αποκλειστικά µε τις τοπολογίες δικτύων διακοπτών.
∆ίκτυα διακοπτών
Ένα δίκτυο διακοπτών µπορεί να αναπαρασταθεί µε ένα γράφο, του οποίου οι κόµβοι
είναι οι επεξεργαστές και οι διακόπτες και οι ακµές είναι οι δυνατές συνδέσεις. Κάθε
επεξεργαστής συνδέεται µε ένα διακόπτη. Οι διακόπτες ενώνουν επεξεργαστές µε άλλους
επεξεργαστές ή διακόπτες.
12
∆.Σ. Βλάχος και Θ.Η. Σίµος
Η άµεση τοπολογία είναι αυτή που ο λόγος του αριθµού των διακοπτών προς τον αριθµό
των επεξεργαστών είναι 1:1. Κάθε διακόπτης ενώνεται µε έναν επεξεργαστή και µε έναν
ή περισσότερους διακόπτες. Στην έµµεση τοπολογία ο λόγος του αριθµού των διακοπτών
προς τον αριθµό των επεξεργαστών είναι µεγαλύτερος από 1:1. Εδώ, µερικοί από τους
διακόπτες ενώνουν άλλους διακόπτες.
Παρακάτω αναφέρουµε µερικά κριτήρια µε τα οποία µπορούµε να αξιολογήσουµε την
αποτελεσµατικότητα ενός δικτύου διακοπτών για την εφαρµογή ενός παράλληλου
αλγόριθµου.
1.
∆ιάµετρος. Η διάµετρος ενός δικτύου είναι η µεγαλύτερη απόσταση
µεταξύ δύο διακοπτών. Η µικρή διάµετρος θεωρείται καλύτερη από την
άποψη ότι µειώνει την πολυπλοκότητα µε την οποία ο παράλληλος
αλγόριθµος θα εδραιώσει την επικοινωνία µεταξύ των επεξεργαστών.
2.
Πλάτος διχοτόµησης. Το πλάτος διχοτόµησης είναι ο ελάχιστος
αριθµός ακµών (συνδέσεων) που πρέπει να αφαιρεθεί από το δίκτυο
διακοπτών για να διαιρεθεί το δίκτυο σε δύο µισά. Το µεγάλο πλάτος
διχοτόµησης θεωρείται καλύτερο από την άποψη ότι όταν έχουµε
µεγάλο όγκο δεδοµένων για µεταφορά, δηµιουργείται µικρότερος
φόρτος ανά σύνδεση.
3.
Ακµές ανά διακόπτη. Είναι καλύτερα ο αριθµός ακµών ανά διακόπτη
να είναι σταθερός και ανεξάρτητος από το µέγεθος του δικτύου γιατί
έτσι µπορούν να οργανωθούν πιο εύκολα οι επεξεργαστές.
4.
Σταθερό µήκος ακµής. Είναι καλύτερα το µήκος των ακµών να είναι
σταθερό για λόγους κλιµάκωσης του δικτύου. Οι κόµβοι και οι ακµές
έτσι, είναι καλύτερα να είναι τοποθετηµένοι στον τρισδιάστατο χώρο.
Παρακάτω, θα εστιάσουµε την προσοχή µας σε έξι από τις πιο δηµοφιλείς τοπολογίες.
∆ίκτυο διδιάστατου πλέγµατος
Στο δίκτυο διδιάστατου πλέγµατος έχουµε µια άµεση τοπολογία στην οποία οι διακόπτες
τοποθετούνται σε ένα διδιάστατο πλέγµα. Η επικοινωνία επιτρέπεται µόνο στους
γειτονικούς διακόπτες. Έτσι, κάθε ένας από τους εσωτερικούς διακόπτες επικοινωνεί µε
τέσσερις, ενώ αυτοί που βρίσκονται στα άκρα µε τρεις. Μια παραλλαγή του δικτύου είναι
οι διακόπτες που βρίσκονται στο τέλος µια γραµµής να ενώνονται µε το διακόπτη που
βρίσκεται στην αρχή της ίδιας γραµµής και το ίδιο για αυτούς που βρίσκονται στα άκρα
µιας στήλης. Ας δούµε λίγο τα τέσσερα κριτήρια για ένα τέτοιο δίκτυο. Υποθέτουµε ότι
το πλέγµα έχει n διακόπτες. Το πλέγµα έχει ελάχιστη διάµετρο και µέγιστο πλάτος
διχοτόµησης όταν είναι τετράγωνο. Τότε και η διάµετρος και το πλάτος διχοτόµησης
είναι ανάλογα του √n. Το δίκτυο έχει σταθερό αριθµό ακµών ανά κόµβο και σταθερό
µήκος ακµής.
13
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 2. Ένα δίκτυο διδιάστατου πλέγµατος (a) και µια παραλλαγή του (b) που διατηρεί
σταθερό τον αριθµό ακµών ανά κόµβο.
∆ίκτυο δυαδικού δέντρου
Σε ένα δίκτυο δυαδικού δέντρου έχουµε n=2d επεξεργαστές που υποστηρίζονται από 2n-1
διακόπτες. Κάθε επεξεργαστής συνδέεται σε κάποιο από τα φύλα του δέντρου. Εδώ
έχουµε το παράδειγµα της έµµεσης τοπολογίας. Οι εσωτερικοί διακόπτες έχουν το πολύ
τρεις συνδέσµους: ένα µε τον πατέρα του αντίστοιχου κόµβου στο δέντρο και δύο µε τα
παιδιά.
Σχήµα 3. Ένα δίκτυο δυαδικού δέντρου. Οι κύκλοι είναι οι διακόπτες και τα τετράγωνα οι
επεξεργαστές.
Το δίκτυο δυαδικού δέντρου έχει διάµετρο 2logn. Το πλάτος όµως διχοτόµησης είναι το
µικρότερο δυνατά, δηλαδή 1. Επίσης τα µήκη των συνδέσµων δεν µπορεί να είναι
σταθερά.
14
∆.Σ. Βλάχος και Θ.Η. Σίµος
∆ίκτυο υπέρ-δέντρου
Το δίκτυο υπέρ-δέντρου είναι µια βελτίωση του δυαδικού δέντρου που εκµεταλλεύεται τη
χαµηλή του διάµετρο αλλά βελτιώνει το µικρό πλάτος διχοτόµησης. Ο καλύτερος τρόπος
για να αποκτήσουµε µια σαφή εικόνα του υπέρ-δέντρου είναι να θεωρήσουµε δύο οπτικές
γωνίες από τις οποίες το παρατηρούµε. Από τη µια, ας πούµε από µπροστά, ένα k-υπέρδέντρο µε βαθµό d µοιάζει µε ένα κλασσικό k-δέντρο. Από το πλάι όµως φαίνεται σαν
ένα αντεστραµµένο δυαδικό δέντρο.
Σχήµα 4. Ένα 4-υπέρ-δέντρο µε βάθος 2 όπως φαίνεται από µπροστά (a) και όπως φαίνεται
από το πλάι (b). Η πλήρης ανάπτυξη του δέντρου φαίνεται στο (c).
Ένα n-υπέρ-δέντρο µε βάθος d έχει 4d επεξεργαστές και 2d(2d-1) διακόπτες. Η διάµετρος
είναι 2d και το πλάτος διχοτόµησης 2d+1. Ο αριθµός ακµών ανά κόµβο δεν µπορεί να
είναι µεγαλύτερος από έξι ενώ πάλι το µήκος των ακµών δεν είναι δυνατόν να παραµένει
σταθερό.
∆ίκτυο πεταλούδας
Το δίκτυο πεταλούδας είναι ένα έµµεσο δίκτυο διακοπτών στο οποίο n=2d επεξεργαστές
συνδέονται µε n(d+1) διακόπτες. Οι διακόπτες χωρίζονται σε d+1 γραµµές µε n
διακόπτες στην κάθε γραµµή. Οι γραµµές αριθµούνται από 0 έως d. Κάθε διακόπτης
ενώνεται µε τέσσερις άλλους διακόπτες (δύο από την προηγούµενη και δύο από την
επόµενη σειρά) εκτός από αυτούς που βρίσκονται στις σειρές 0 και d.
Έστω ο κόµβος (i,j) αναφέρεται στο διακόπτη που βρίσκεται στη σειρά I και στη στήλη j.
Τότε ο κόµβος(i,j) ενώνεται µε τον κόµβο(i-1,j) και µε τον κόµβο(i-1,m) της
προηγούµενης σειράς, αν το m είναι ο ακέραιος που προκύπτει αντιστρέφοντας το i πιο
σηµαντικό ψηφίο της αναπαράστασης του j στο δυαδικό σύστηµα αρίθµησης. Έτσι, για
παράδειγµα, ο κόµβος(2,3) ενώνεται µε τον κόµβο (1,3) και µε τον κόµβο(1,1) αφού το
j=3=011 και αντιστρέφοντας το i=2 πιο σηµαντικό ψηφίο προκύπτει το 1 (011→001).
15
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σηµειώστε εδώ πως αν ο κόµβος(i,j) ενώνεται µε τον κόµβο(i-1,m) τότε ο κόµβος(i,m)
ενώνεται µε τον κόµβο(i-1,j).
Σχήµα 5. Ένα δίκτυο πεταλούδας µε 8 επεξεργαστές και 32 διακόπτες.
Ας υποθέσουµε ότι η σειρά 0 ταυτίζεται µε τη σειρά d. Τότε η διάµετρος του δικτύου
είναι d=logn και το πλάτος διχοτόµησης είναι n/2.
Ένας απλός αλγόριθµος για τη δροµολόγηση µηνυµάτων σε ένα δίκτυο πεταλούδας είναι
ο ακόλουθος: Κάθε διακόπτης επιλέγει από το µήνυµα το πρώτο ψηφίο. Αν είναι 0
στέλνει το υπόλοιπο µήνυµα στον κάτω και αριστερά κόµβο µε τον οποίο συνδέεται ενώ
αν είναι 1 στον κάτω και δεξιά κόµβο. Ας υποθέσουµε ότι ο επεξεργαστής 010 (2) θέλει
να στείλει ένα µήνυµα στον επεξεργαστή 101 (5). Τότε, τοποθετεί τα ψηφία 101 στην
αρχή του µηνύµατος και στέλνει το νέο µήνυµα στον διακόπτη µε τον οποίο συνδέεται.
Το µήνυµα αρχικά πηγαίνει στον διακόπτη (0,2). Επειδή το µήνυµα αρχίζει µε 1, ο
κόµβος αφαιρεί αυτό το 1 από το µήνυµα και στέλνει το υπόλοιπο στον κόµβο (1,6).
Αυτός µε τη σειρά του αφαιρεί το 0 από το µήνυµα και το στέλνει στον κόµβο (2,4), ο
οποίος µε τη σειρά του αφαιρεί το 1 από το µήνυµα και το στέλνει στον κόµβο (3,5) που
συνδέεται µε τον επεξεργαστή 5 και είναι ο τελικός προορισµός του µηνύµατος.
Στο παρακάτω σχήµα φαίνεται η διαδικασία δροµολόγησης που περιγράψαµε.
16
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 6. ∆ροµολόγηση ενός µηνύµατος από τον επεξεργαστή 2 στον επεξεργαστή 5 σε ένα
δίκτυο πεταλούδας. Οι έντονες γραµµές είναι το µονοπάτι που ακολουθεί το µήνυµα.
∆ίκτυο υπέρ-κύβου
Το δίκτυο υπέρ-κύβου ή αλλιώς δυαδικού n-κύβου, είναι ένα δίκτυο πεταλούδας στο
οποίο η κάθε στήλη έχει συσταλθεί σε ένα κόµβο. Ο δυαδικός n-κύβος αποτελείται από
n=2d επεξεργαστές και από ίσο αριθµό διακοπτών (άµεση τοπολογία). Οι επεξεργαστές
µαζί µε τον διακόπτη που τους αντιστοιχεί αριθµούνται από 0 έως 2d-1. Ένας κόµβος
(διακόπτης) ενώνεται µε κάποιον άλλο αν διαφέρουν µόνο σε ένα bit της δυαδικής
αναπαράστασης του αριθµού που τους αντιστοιχεί.
Η διάµετρος του δυαδικού n-κύβου είναι d=logn και το πλάτος διχοτόµησης είναι n/2.
Ενώ έχουµε πετύχει µικρή διάµετρο και µεγάλο πλάτος διχοτόµησης, τόσο ο αριθµός
ακµών ανά κόµβο δεν είναι σταθερός (logn) όσο και το µήκος των ακµών µεγαλώνει µε
το µέγεθος του δικτύου. Το µεγάλο προτέρηµα όµως του δυαδικού n-κύβου είναι η
ευκολία δροµολόγησης και η εύρεση της µικρότερης διαδροµής µεταξύ δύο κόµβων.
Έτσι, αν η δυαδική αναπαράσταση της αρίθµησης δύο επεξεργαστών διαφέρει σε k
ψηφία, τότε το µικρότερο µονοπάτι που τους συνδέει αποτελείται από k ακµές. Για
παράδειγµα η µικρότερη διαδροµή από τον επεξεργαστή 0101 στον 0011 είναι 2 αφού
διαφέρουν σε δύο ψηφία και το µονοπάτι που τους συνδέει είναι:
0101→0111→0011
17
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 7. Ένας υπέρ-κύβος µε 16 επεξεργαστές.
∆ίκτυο ανταλλαγής-αναδιάταξης
Η τελευταία τοπολογία που θα αναφέρουµε βασίζεται στην ιδέα της τέλειας αναδιάταξης.
Φανταστείτε ότι έχετε 8 κάρτες που αριθµούνται από 0 έως 7. Χωρίζετε τις κάρτες σε δύο
σύνολα, στο πρώτο έχετε τις κάρτες από τ0 0 έως το 3 και στο άλλο από το 4 έως το 7.
Ύστερα ανακατεύετε τα δύο σύνολα µε τον καλύτερο δυνατό τρόπο, παίρνοντας δηλαδή
εναλλάξ µία κάρτα από κάθε σύνολο.
Σχήµα 8. Ένα σύνολο καρτών και το τέλειο ανακάτεµά τους
Αν αντιστοιχίσουµε σε κάθε κάρτα το δυαδικό αριθµό που αντιστοιχεί στην αρχική της
θέση, τότε η τελική της θέση θα δίνεται από µια κυκλική µετάθεση της αρχικής θέσης
προς τα αριστερά. Έτσι, η κάρτα που αρχικά ήταν στη θέση 5 (101) θα βρεθεί µετά το
ανακάτεµα στη θέση 3 (011).
18
∆.Σ. Βλάχος και Θ.Η. Σίµος
Το δίκτυο ανταλλαγής- αναδιάταξης είναι µια άµεση τοπολογία µε n=2d επεξεργαστές
και διακόπτες που αποτελούν τους κόµβους του δικτύου. Κάθε κόµβος αριθµείται από το
0 έως το n-1. Οι διακόπτες έχουν δύο ειδών συνδέσεις: τις συνδέσεις ανταλλαγής όπου
δύο διακόπτες συνδέονται µόνο αν διαφέρουν στο λιγότερο σηµαντικό τους ψηφίο και τις
συνδέσεις αναδιάτάξης όπου δύο διακόπτες συνδέονται µόνο αν ο ένας είναι κυκλική
µετάθεση του άλλου προς τα αριστερά.
Κάθε διακόπτης στο δίκτυο ανταλλαγής-αναδιάταξης έχει σταθερό αριθµό συνδέσεων:
δύο εξερχόµενων και δύο εισερχόµενων χωρίς να υπολογίζουµε και τις συνδέσεις µε τους
αντίστοιχους επεξεργαστές. Το µήκος της πιο µεγάλης ακµής αυξάνεται µε το µέγεθος
του δικτύου. Η διάµετρος του δικτύου µε n επεξεργαστές είναι 2logn-1 και το πλάτος
διχοτόµησης ≈n/logn.
Σχήµα 9. Ένα δίκτυο ανταλλαγής-αναδιάταξης µε 16 επεξεργαστές. Τα τόξα δείχνουν τις
µονοκατευθυντικές συνδέσεις αναδιάταξης ενώ οι γραµµές τις συνδέσεις ανταλλαγής.
Ένας απλός αλγόριθµος δροµολόγησης µηνυµάτων σε ένα δίκτυο ανταλλαγήςαναδιάταξης έχει να κάνει µε την εύρεση ενός συνδυασµού από διαδροµές ανταλλαγής ή
αναδιάταξης για να φτάσουµε στον τελικό προορισµό. Η χειρότερη περίπτωση είναι, για
παράδειγµα σε ένα δίκτυο µε 16 επεξεργαστές, να πάµε από τον επεξεργαστή 0 στον
επεξεργαστή 15, όπου θα έχουµε 2log16-1=7 b;hmataQ
0000 (Ε) 0001 (S) 0010 (Ε) 0011 (S) 0110 (Ε) 0111 (S) 1110 (Ε) 1111
όπου το (Ε) δείχνει ανταλλαγή και το (S) αναδιάταξη.
19
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ανακεφαλαιώνοντας:
Επεξεργαστές
∆ιακόπτες
∆ιάµετρος
Πλάτος
∆ιχοτόµησης
Ακµές/
κόµβο
Σταθερό
µήκος
ακµής
2D πλέγµα
n=d
2
n
2(√n -1)
√n
4
Ναι
∆υαδικό δέντρο
n=2
d
2n-1
2logn
1
3
Όχι
n=4
d
2n-√n
logn
n/2
6
Όχι
Πεταλούδα
n=2
d
n(logn + 1)
logn
n/2
4
Όχι
Υπέρ-κύβος
n=2
d
n
logn
n/2
logn
Όχι
k
n
2logn - 1
≈n/logn
2
Όχι
Υπέρ-δέντρο
Ανταλλαγής Αναδιάταξης
n=2
Συγκεντρωτικά στοιχεία για τα έξι δίκτυα διακοπτών που αναπτύχθηκαν.
Μήτρες επεξεργαστών
Ένας διανυσµατικός υπολογιστής είναι ένας υπολογιστής του οποίου το σύνολο των
εντολών περιέχει λειτουργίες τόσο σε βαθµωτά όσο και σε διανυσµατικά µεγέθη.
Υπάρχουν δύο τρόποι να υλοποιήσει κανείς ένα διανυσµατικό υπολογιστή. Ο πρώτος
είναι µε χρήση της τεχνικής pipeline όπου ο επεξεργαστής φέρνει από τη µνήµη ένα
σύνολο δεδοµένων στο οποίο γίνονται πράξεις από µια pipeline αριθµητική µονάδα. Οι
υπολογιστές Gray-1 και Cyber-205 είναι γνωστά παραδείγµατα αυτού του τύπου
επεξεργαστών.
Ο δεύτερος τρόπος είναι µε τη χρήση µιας µήτρας επεξεργαστών. Η µήτρα επεξεργαστών
είναι ένας διανυσµατικός υπολογιστής ο οποίος υλοποιείται σαν ένας σειριακός
υπολογιστής που συνδέεται σε ένα σύνολο ταυτόσηµων και συγχρονισµένων
υπολογιστικών µονάδων, κάθε µια από τις οποίες µπορεί να κάνει την ίδια λειτουργία σε
διαφορετικά δεδοµένα. Το βασικό κίνητρο για την ανάπτυξη των µητρών επεξεργαστών
είναι η παρατήρηση ότι µεγάλο ποσοστό των επιστηµονικών υπολογισµών µπορούν να
υλοποιηθούν µε παραλληλισµό δεδοµένων.
Αρχιτεκτονική και παραλληλισµός δεδοµένων
Στο σχήµα 10 φαίνεται η γενική αρχιτεκτονική µιας µήτρας επεξεργαστών. Είναι ένα
σύνολο από απλές υπολογιστικές µονάδες που συνδέονται µε ένα κεντρικό υπολογιστή.
Ο κεντρικός υπολογιστής είναι ένας τυπικός υπολογιστής. Η κεντρική του µνήµη περιέχει
τις εντολές που πρόκειται να εκτελεστούν καθώς και τα απαραίτητα δεδοµένα για
σειριακούς υπολογισµούς που γίνονται στον κεντρικό υπολογιστή. Η µήτρα
επεξεργαστών περιέχει ένα σύνολο από ζευγάρια υπολογιστικών µονάδων µε τις µνήµες
τους. Τα δεδοµένα πάνω στα οποία γίνονται παράλληλοι υπολογισµοί κατανέµονται σε
αυτές τις µνήµες. Ο κεντρικός υπολογιστής µεταδίδει στη µήτρα των επεξεργαστών τις
απαραίτητες εντολές που πρόκειται να εκτελεστούν παράλληλα (κάθε επεξεργαστής
τρέχει τις ίδιες εντολές).
Ας υποθέσουµε για παράδειγµα ότι η µήτρα των επεξεργαστών περιέχει 1024
επεξεργαστές που ονοµάζονται p0, p1, …,p1023. Έστω ότι έχουµε δύο διανύσµατα Α,Β
20
∆.Σ. Βλάχος και Θ.Η. Σίµος
κάθε ένα από τα οποία έχει 1024 στοιχεία. Τα στοιχεία αi,βi τοποθετούνται στη µνήµη
του επεξεργαστή pi. Τότε, η µήτρα επεξεργαστών µπορεί µε µια εντολή να υπολογίσει το
διάνυσµα Α+Β, όπου κάθε στοιχείο του διανύσµατος υπολογίζεται σε έναν επεξεργαστή.
Σηµειώστε εδώ πως αν η διάσταση των διανυσµάτων Α,Β είναι µικρότερη από 1024,
απαιτείται πάλι ο ίδιος χρόνος (µία εντολή) για τον υπολογισµό του Α+Β. Τι γίνεται όµως
στην περίπτωση που τα διανύσµατα Α,Β έχουν διάσταση µεγαλύτερη από 1024;
Σχήµα 10. Γενική αρχιτεκτονική µιας µήτρας επεξεργαστών.
Για να δούµε πως αντιµετωπίζεται αυτό το πρόβληµα, ας υποθέσουµε ότι η διάσταση των
διανυσµάτων Α,Β είναι 10000. Τότε, µπορούµε σε 784 επεξεργαστές της µήτρας να
αντιστοιχίσουµε 10 στοιχεία και στους υπόλοιπους 240 επεξεργαστές από 9 στοιχεία
(πράγµατι 784x10+240x9=1000). Η κατανοµή των στοιχείων γίνεται είτε αυτόµατα είτε
µε ευθύνη του προγραµµατιστή.
Απόδοση της µήτρας επεξεργαστών
Με τον όρο απόδοση εννοούµε ένα µέτρο των λειτουργιών που εκτελούνται στη µονάδα
του χρόνου. Μπορούµε να µετρήσουµε την απόδοση σε εντολές ανά δευτερόλεπτο. Η
απόδοση µιας µήτρας επεξεργαστών εξαρτάται από το βαθµό χρήσης των επεξεργαστών.
Η µέγιστη απόδοση επιτυγχάνεται όταν όλοι οι επεξεργαστές είναι ενεργοί και το µέγεθος
των δεδοµένων ακέραιο πολλαπλάσιο του πλήθους των επεξεργαστών.
21
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Παραδείγµατα:
Έστω µια µήτρα επεξεργαστών περιέχει 1024 επεξεργαστές, κάθε ένας από
τους οποίου µπορεί να εκτελέσει την πρόσθεση δύο ακεραίων σε 1µsec. Ποια
είναι η απόδοση της µήτρας όταν πρέπει να προσθέσουµε δύο ακέραια
διανύσµατα µήκους 1024 στοιχείων;
Απάντηση:
Το πλήθος των λειτουργιών είναι 1024 και µπορούν να γίνουν όλες µαζί.
Έτσι:
1024 ΛΕΙΤΟΥΡΓΙΕΣ
ΑΠΟ∆ΟΣΗ =
= 1, 024 ⋅109 ΛΕΙΤΟΥΡΓΙΕΣ / sec
1µ sec
Έστω µια µήτρα επεξεργαστών περιέχει 512 επεξεργαστές, κάθε ένας από
τους οποίου µπορεί να προσθέσει δύο ακέραιους σε 1µsec. Ποια η απόδοση
της µήτρας όταν έχει να προσθέσει δύο ακέραια διανύσµατα µήκους 600
στοιχείων;
Απάντηση:
Αναγκαστικά 88 από τους επεξεργαστές θα κάνουν 2 προσθέσεις γιατί
424+2x88=600. Έτσι:
ΑΠΟ∆ΟΣΗ =
600 ΛΕΙΤΟΥΡΓΙΕΣ
= 3 ⋅108
2 µ sec
ΛΕΙΤΟΥΡΓΙΕΣ / sec
∆ίκτυα διασύνδεσης επεξεργαστών
Είναι σαφές ότι όλες οι παράλληλες λειτουργίες δεν είναι τόσο απλές όσο η παράλληλη
πρόσθεση δύο διανυσµάτων. Για παράδειγµα, µια εξίσωση πεπερασµένων διαφορών της
µορφής
ai=(ai-1+ai+1)/2
απαιτεί την επικοινωνία του i-επεξεργαστή µε τους επεξεργαστές (i-1) και (i+1). Έτσι,
πρέπει να υπάρχει ένα δίκτυο επικοινωνίας µεταξύ των επεξεργαστών ώστε να µπορούν
να ανταλλάσσουν δεδοµένα. Η πιο συνηθισµένη τοπολογία είναι το διδιάστατο πλέγµα
που επιτρέπει τη σύνθεση της µήτρας από ολοκληρωµένα (VLSI) υπό-πλέγµατα
σταθερού µήκους και πλάτους όπως φαίνεται στο σχήµα 11.
Σχήµα 11. Σύνθεση µιας µήτρας 96 επεξεργαστών από 6 chip των 16 επεξεργαστών.
22
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ενεργοποιώντας και απενεργοποιώντας επεξεργαστές
Παρ’ όλο που σε µια µήτρα επεξεργαστών όλοι οι επεξεργαστές εκτελούν ταυτόχρονα
την ίδια εντολή, υπάρχουν περιπτώσεις που κάποιοι από αυτούς δεν πρέπει να
εκτελέσουν µια συγκεκριµένη εντολή. Για το λόγο αυτό, κάθε επεξεργαστής έχει ένα bit
ενεργοποίησης που του επιτρέπει ή όχι να εκτελέσει την επόµενη εντολή.
Για παράδειγµα ας υποθέσουµε ότι σε µια µήτρα επεξεργαστών έχουµε µοιράσει τα
στοιχεία του διανύσµατος Α, ένα στοιχείο σε κάθε επεξεργαστή. Η λειτουργία που
θέλουµε να εκτελέσουµε είναι η ακόλουθη: Θέλουµε να µετατρέψουµε κάθε µη µηδενικό
στοιχείο του Α σε 1 και κάθε µηδενικό στοιχείο σε -1. Αρχικά οι επεξεργαστές ελέγχουν
αν το στοιχείο που διαθέτουν είναι 0. Αν είναι, απενεργοποιούν το bit ενεργοποίησής
τους. Στη συνέχεια οι ενεργοποιηµένοι επεξεργαστές µετατρέπουν το στοιχείο τους σε 1.
Έπειτα, όλοι οι επεξεργαστές αντιστρέφουν το bit ενεργοποίησής τους. Τέλος, οι
ενεργοποιηµένοι επεξεργαστές (αυτοί δηλαδή που έχουν µηδενικά στοιχεία) µετατρέπουν
τα στοιχεία τους σε -1. Τελικά, όλοι οι επεξεργαστές ενεργοποιούν το bit ενεργοποίησής
τους.
Σχήµα 12. Εκτέλεση της εντολής if-then-else. Οι σκιασµένοι επεξεργαστές είναι
απενεργοποιηµένοι.
Είναι σαφές ότι η απόδοση της µήτρας επεξεργαστών µειώνεται δραµατικά όταν
πρόκειται να εκτελεστεί κώδικας υπό συνθήκη. Στην περίπτωση που πρόκειται να
εκτελεστεί µια παράλληλη δοµή if-then-else πρέπει όσοι από τους επεξεργαστές
ικανοποιούν τη συνθήκη του if να εκτελέσουν τις εντολές που ακολουθούν το then και
στη συνέχεια οι υπόλοιποι επεξεργαστές να εκτελέσουν τις εντολές που ακολουθούν το
else. Σε κάθε περίπτωση η απόδοση είναι µικρότερη της µισής απόδοσης που θα
επιτυγχάνονταν αν όλες οι εντολές εκτελούνταν παράλληλα.
Αδυναµίες της µήτρας επεξεργαστών
Οι µήτρες επεξεργαστών έχουν σηµαντικές αδυναµίες, πράγµα που κάνει τους
επιστήµονες επιφυλακτικούς στη χρήση τους. Μερικές από αυτές είναι:
•
∆εν παρουσιάζουν όλα τα προβλήµατα παραλληλισµό δεδοµένων. Τα
προβλήµατα που παρουσιάζουν συναρτησιακό παραλληλισµό δεν
τρέχουν αποδοτικά στις µήτρες επεξεργαστών.
23
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
•
Τµήµατα του κώδικα που εκτελούνται υπό συνθήκη δεν τρέχουν
αποδοτικά.
•
Οι µήτρες επεξεργαστών δεν ενδείκνυνται για χρήση από πολλούς
χρήστες.
•
Όσον αφορά στο κόστος τους, µόνο τα πολύ µεγάλα συστήµατα
συµφέρουν.
•
Οι επεξεργαστές που χρησιµοποιούνται στις µήτρες επεξεργαστών δεν
µπορούν να παρακολουθήσουν τις εξελίξεις των εµπορικών
επεξεργαστών.
•
Το αρχικό κίνητρο για την ανάπτυξη των µητρών επεξεργαστών ήταν οι
ακριβές CPU, πράγµα που δεν υφίσταται σήµερα.
Πολύ-επεξεργαστές
Ένας πολύ-επεξεργαστής είναι ένα σύστηµα µε πολλούς επεξεργαστές και µια κοινή
µνήµη. Η ίδια διεύθυνση µνήµης για δύο διαφορετικούς επεξεργαστές αντιστοιχίζεται
στην ίδια διεύθυνση της κοινής µνήµης. Οι πολύ-επεξεργαστές λύνουν τρία από τα
βασικά προβλήµατα που συναντώνται στις µήτρες επεξεργαστών: Χτίζονται από
εµπορικούς επεξεργαστές, υποστηρίζουν πολλαπλούς χρήστες και λειτουργούν
αποδοτικά στην περίπτωση κώδικα που εκτελείται υπό συνθήκη. Θα ασχοληθούµε µε δύο
είδη πολύ-επεξεργαστών: τους κεντρικούς πολύ-επεξεργαστές, στους οποίους όλη η
κοινή µνήµη είναι σε ένα σηµείο και στους κατανεµηµένους πολύ-επεξεργαστές, στους
οποίους η κοινή µνήµη είναι κατανεµηµένη µεταξύ των επεξεργαστών.
Κεντρικοί πολύ-επεξεργαστές
Σε έναν τυπικό επεξεργαστή, υπάρχει ένας διάδροµος που συνδέει τη CPU µε την κύρια
µνήµη καθώς επίσης και οι περιφερειακές συσκευές εισόδου/εξόδου. Μια γρήγορη µνήµη
επίσης, βοηθά να αυξάνεται ο βαθµός χρήσης της CPU µε το να µειώνεται ο χρόνος στον
οποίο η CPU είναι ανενεργή περιµένοντας την ανάκτηση εντολών και δεδοµένων από την
κύρια µνήµη.
Ένας κεντρικός πολύ-επεξεργαστής είναι µια φυσική επέκταση αυτού του συστήµατος.
Επιπλέον επεξεργαστές µαζί µε τη γρήγορη µνήµη τους συνδέονται στο διάδροµο που
τους ενώνει µε την κύρια µνήµη. Όπως αναφέραµε κάθε επεξεργαστής έχει τη δική του
κύρια µνήµη. Η αρχιτεκτονική αυτή λέγεται οµοιόµορφης προσπέλασης µνήµης
(Uniform Access Memory, UMA) ή και συµµετρικών επεξεργαστών (Symmetric MultiProcessor, SMP). Οι κεντρικοί επεξεργαστές είναι πρακτικοί γιατί η παρουσία της
γρήγορης µνήµης εξασφαλίζει ότι κάθε επεξεργαστής δεν προσθέτει ιδιαίτερο φόρτο στο
διάδροµο. Υπάρχει βέβαια ένα άνω όριο στον αριθµό των επεξεργαστών που
χρησιµοποιούνται, πάνω από το οποίο δε γίνεται αποδοτική χρήση του κοινού διαδρόµου.
24
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 13. Η γενική αρχιτεκτονική ενός κεντρικού πολύ-επεξεργαστή.
Τα ιδιαίτερα δεδοµένα είναι δεδοµένα που µπορούν να χρησιµοποιηθούν µόνο από έναν
επεξεργαστή ενώ τα κοινά δεδοµένα είναι αυτά που µπορούν να χρησιµοποιηθούν από
όλους. Στους κεντρικούς πολύ-επεξεργαστές, οι επεξεργαστές επικοινωνούν µεταξύ τους
µέσω των κοινών δεδοµένων. Για παράδειγµα, οι επεξεργαστές µπορούν να
συνεργάζονται για να εκτελέσουν µια λειτουργία στα στοιχεία µιας συνδεδεµένης λίστας.
Υπάρχει ένας κοινός δείκτης που δείχνει πάντα το επόµενο στοιχείο στη λίστα που
πρόκειται να του γίνει επεξεργασία. Κάθε επεξεργαστής ανακτά το περιεχόµενο του
κοινού δείκτη και το αυξάνει κατά ένα πριν το προσπελάσει ο επόµενος επεξεργαστής.
Οι σχεδιαστές των κεντρικών πολύ-επεξεργαστών έχουν να αντιµετωπίσουν δύο
προβλήµατα: τη συνέπεια της γρήγορης µνήµης και το συγχρονισµό.
Συνέπεια της γρήγορης µνήµης.
Αντιγράφοντας κοινά δεδοµένα στις γρήγορες µνήµες πολλών επεξεργαστών µπορεί να
προκύψει το ακόλουθο πρόβληµα: Επειδή η εικόνα που έχει ο κάθε επεξεργαστής για τη
κοινή µνήµη είναι µέσα στη γρήγορη µνήµη του, πρέπει να εξασφαλιστεί ότι τα
περιεχόµενα θα είναι τα ίδια για όλους τους επεξεργαστές όταν αναφέρονται στο ίδιο
τµήµα της κοινής κύριας µνήµης. Το πρόβληµα αποτυπώνεται στο σχήµα 14.
Η «κατασκοπία» (snooping) είναι µια τεχνική που χρησιµοποιείται για τη λύση του
προβλήµατος. Κάθε επεξεργαστής παρακολουθεί το διάδροµο για να δει ποιες
διευθύνσεις της µνήµης µεταφέρονται κάθε στιγµή. Για να γράψει ένας επεξεργαστής στη
γρήγορη µνήµη, πρέπει να λάβει δικαίωµα για προσπέλαση στην περιέχει της κύριας
µνήµης που αντιστοιχεί. Τότε, οι άλλοι επεξεργαστές που έχουν στη γρήγορη µνήµη τους
την ίδια περιοχή της κύριας µνήµης, το σηµειώνουν σαν µη ενηµερωµένο. Στη συνέχεια,
αν κάποιος επεξεργαστής ζητήσει δεδοµένα από ένα µη ενηµερωµένο τµήµα της
γρήγορης µνήµης, τότε πρώτα ενηµερώνεται και µετά γίνεται η προσπέλαση.
25
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 14. Παράδειγµα του προβλήµατος της συνέπειας µνήµης. (α) Η µνήµη Χ περιέχει το
7. (b) η CPU Α διαβάζει το Χ. (c) η CPU B γράφει στο Χ το 2. (d) η CPU Α έχει λάθος
τιµή.
Συγχρονισµός επεξεργαστών.
Ο συγχρονισµός µεταξύ των επεξεργαστών είναι αναγκαίος, όταν αυτοί συνεργάζονται
για µια λειτουργία. Ο αµοιβαίος αποκλεισµός είναι µια τεχνική κατά την οποία µόνο ένας
επεξεργαστής έχει πρόσβαση σε µια ενέργεια µια δεδοµένη στιγµή. Στο προηγούµενο
παράδειγµα µε την επεξεργασία των στοιχείων µιας λίστας, αµοιβαίος αποκλεισµός
πρέπει να εφαρµοστεί στο σηµείο που οι επεξεργαστές επιχειρούν να ανακτήσουν και να
αυξήσουν το περιεχόµενο του κοινού δείκτη.
Ο συγχρονισµός φράγµατος είναι µια άλλη τεχνική που χρησιµοποιείται για το
συγχρονισµό των επεξεργαστών. Σε αυτήν, υπάρχει ένα σηµείο στον κώδικα που εκτελεί
ο κάθε επεξεργαστής από το οποίο δεν µπορεί να συνεχίσει κανένας την λειτουργία του,
αν όλοι οι επεξεργαστές δεν έχουν φτάσει σε αυτό το σηµείο.
Κατανεµηµένοι πολύ-επεξεργαστές
Η παρουσία του κοινού διαδρόµου στους κεντρικούς πολύ-επεξεργαστές θέτει ένα όριο
στον αριθµό των επεξεργαστών που µπορούν να χρησιµοποιηθούν ώστε η χρήση του
διαδρόµου να γίνεται αποδοτικά. Η εναλλακτική λύση είναι η κατανοµή της κύριας
µνήµης στους διάφορους επεξεργαστές. Επειδή η εκτέλεση των προγραµµάτων έχει
συχνά µεγάλη χωρική και χρονική τοπικότητα, το πιο πιθανό όταν κατανέµει κανείς
κώδικα και δεδοµένα στους επεξεργαστές, είναι να µειωθεί σηµαντικά ο αριθµός
προσπελάσεων σε µνήµη διαφορετική από την τοπική. Το γεγονός αυτό αυξάνει τον
αριθµό των επεξεργαστών που µπορούν να χρησιµοποιηθούν.
Τα συστήµατα στα οποία η κατανεµηµένη τοπική µνήµη των επεξεργαστών αποτελεί την
κύρια µνήµη του συστήµατος των πολλών επεξεργαστών ονοµάζονται κατανεµηµένοι
πολύ-επεξεργαστές. Στα συστήµατα αυτά η ίδια διεύθυνση µνήµης σε διαφορετικούς
επεξεργαστές αντιστοιχεί στην ίδια διεύθυνση της κύριας µνήµης. Τα συστήµατα αυτά
ονοµάζονται και ανοµοιόµορφης προσπέλασης µνήµης (Non Uniform Memory Access,
NUMA) και ο χρόνος προσπέλασης σε µια περιοχή της κύριας µνήµης εξαρτάται από το
σε ποιον επεξεργαστή βρίσκεται κατανεµηµένη αυτή η περιοχή.
Το πρόβληµα βέβαια της συνέπειας της τοπικής µνήµης παραµένει. Η πιο διαδεδοµένη
τεχνική για την αντιµετώπισή του στα συστήµατα κατανεµηµένων πολύ-επεξεργαστών
26
∆.Σ. Βλάχος και Θ.Η. Σίµος
είναι το λεγόµενο πρωτόκολλο φακέλων. Εδώ, ένας φάκελος περιέχει πληροφορίες για
την από κοινού χρήση κάθε τµήµατος της κύριας µνήµης. Για κάθε τέτοιο τµήµα, ο
φάκελος δείχνει:
1.
αν το συγκεκριµένο τµήµα δεν βρίσκεται σε κανέναν επεξεργαστή
2.
αν το συγκεκριµένο τµήµα βρίσκεται σε έναν ή περισσότερους
επεξεργαστές και το περιεχόµενό του έχει ενηµερωθεί
3.
αν το συγκεκριµένο τµήµα ανήκει αποκλειστικά σε έναν επεξεργαστή ο
οποίος έχει µεταβάλλει το περιεχόµενό του
Ας δούµε σε έναν παράδειγµα πως λειτουργεί αυτή η τεχνική: Θεωρείστε το
κατανεµηµένο σύστηµα του σχήµατος 15.
Σχήµα 15. Γενική αρχιτεκτονική ενός συστήµατος κατανεµηµένων επεξεργαστών.
Το σύστηµα έχει τρεις επεξεργαστές, κάθε ένας από τους οποίους έχει µια γρήγορη
µνήµη, µια τοπική µνήµη και έναν φάκελο. Οι τρεις κατανεµηµένες τοπικές µνήµες
αποτελούν την κύρια µνήµη του συστήµατος και κάθε επεξεργαστής µπορεί να
προσπελάσει κάθε θέση της. Η ακέραια µεταβλητή Χ βρίσκεται στη µνήµη του
επεξεργαστή 2 και έχει αρχική τιµή 7. Ο φάκελος του επεξεργαστή 2 δείχνει ότι η
αντίστοιχη θέση δε βρίσκεται σε κανέναν άλλο επεξεργαστή. Οι τιµές των φακέλων
ερµηνεύονται ως εξής:
•
U : δείχνει ότι δεν υπάρχει σε καµία άλλη τοπική µνήµη
•
Sxxx : δείχνει ότι µοιράζεται από τους επεξεργαστές που δείχνουν τα
ψηφία xxx
•
Exxx : δείχνει ότι βρίσκεται αποκλειστικά στον επεξεργαστή xxx και
το περιεχόµενό της έχει αλλάξει.
Στο σχήµα 16 φαίνονται οι διάφορες καταστάσεις του φακέλου σε διάφορες στιγµές της
λειτουργίας του συστήµατος.
27
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 16. (a) Η διεύθυνση Χ έχει την τιµή 7 και δε βρίσκεται σε καµία γρήγορη µνήµη. (b)
Ο επεξεργαστής 0 διαβάζει την τιµή. (c) Ο επεξεργαστής 2 διαβάζει την τιµή. (d) Ο
επεξεργαστής 0 γράφει στο Χ την τιµή 6. (e) Ο επεξεργαστής 1 διαβάζει το Χ. (f) Ο
επεξεργαστής 2 γράφει στο Χ την τιµή 5. (g) Ο επεξεργαστής 0 γράφει στο Χ την τιµή 4. (h)
Η διεύθυνση Χ δε βρίσκεται σε καµία γρήγορη µνήµη.
Συστήµατα πολλών υπολογιστών
Ένα σύστηµα πολλών υπολογιστών είναι ένα παράδειγµα κατανεµηµένου συστήµατος. Η
διαφορά εδώ όµως είναι ότι η µνήµη κάθε υπολογιστή δεν αντιστοιχεί σε κάποιο τµήµα
της κεντρικής µνήµης αλλά είναι ανεξάρτητη για κάθε υπολογιστή. Έτσι, η ίδια
διεύθυνση µνήµης σε διαφορετικούς υπολογιστές αντιστοιχεί σε διαφορετική µνήµη στη
συνολική µνήµη του συστήµατος. Κάθε υπολογιστής µπορεί να προσπελάσει µόνο τη
δική του µνήµη και η επικοινωνία µεταξύ των υπολογιστών γίνεται µε ανταλλαγή
µηνυµάτων.
Τα εµπορικά συστήµατα πολλών υπολογιστών συνήθως προσφέρουν ένα δίκτυο
διακοπτών για αποδοτική επικοινωνία µεταξύ των υπολογιστών. Τα εµπορικά συστήµατα
προσφέρουν µια ισορροπία µεταξύ της ταχύτητας των υπολογιστών και της ταχύτητας
επικοινωνίας. Σε αντίθεση, τα συστήµατα clusters αποτελούνται από υλικά που
χρησιµοποιούνται για κατασκευή τοπικών δικτύων. Αυτό έχει σαν αποτέλεσµα να έχουν
28
∆.Σ. Βλάχος και Θ.Η. Σίµος
µειωµένο κόστος αλλά η ταχύτητα επεξεργασίας και επικοινωνίας πολλές φορές να είναι
σε αναντιστοιχία.
Ασύµµετρα συστήµατα πολλών υπολογιστών
Τα συστήµατα αυτά µοιάζουν µε τις µήτρες επεξεργαστών.
Σχήµα 17. Ένα ασύµµετρο σύστηµα πολλών υπολογιστών.
Υπάρχει ένας κεντρικός υπολογιστής, ένα σύστηµα διασύνδεσης και οι ανεξάρτητοι
υπολογιστές. Ο κύριος υπολογιστής είναι αυτός µε τον οποίο αλληλεπιδρά ο χρήστης. Οι
ανεξάρτητοι υπολογιστές αναλαµβάνουν µόνο την παράλληλη εκτέλεση προγραµµάτων.
Σηµειώνουµε εδώ πως οι ανεξάρτητοι υπολογιστές είναι συµβατικοί υπολογιστές µε
συµβατικά λειτουργικά συστήµατα. Αυτό έχει σαν αποτέλεσµα να µετατίθεται στο
χρήστη η ευθύνη της ανάπτυξης του παράλληλου προγράµµατος.
Το βασικό µειονέκτηµα αυτών των συστηµάτων είναι ότι αν για κάποιο λόγο ο κύριος
υπολογιστής είναι εκτός λειτουργίας, τότε όλο το σύστηµα δε λειτουργεί. Επιπλέον, ο
αριθµός των χρηστών επηρεάζει την ταχύτητα λειτουργίας του κύριου υπολογιστή,
πράγµα που επιδρά αρνητικά στην αποδοτικότητα του συστήµατος.
Ένα άλλο µειονέκτηµα είναι ότι τα προγράµµατα σε αυτά τα συστήµατα είναι δύσκολο
να διορθωθούν. Αυτό συµβαίνει γιατί οι ανεξάρτητοι υπολογιστές µπορούν να
παρακολουθούνται µόνο µέσω του κύριου υπολογιστή. Τέλος, για κάθε εφαρµογή ο
χρήστης πρέπει να γράψει δύο προγράµµατα, ένα για τον κύριο και ένα για τους
ανεξάρτητους υπολογιστές.
Συµµετρικά συστήµατα πολλών υπολογιστών
Στα συστήµατα αυτά κάθε υπολογιστής τρέχει το ίδιο λειτουργικά σύστηµα και µπορεί να
κάνει τις ίδιες λειτουργίες. Ο χρήστης µπορεί να εργαστεί σε οποιοδήποτε υπολογιστή
και κάθε ένας από τους υπολογιστές µπορεί να συµµετέχει στην παράλληλη λύση ενός
προβλήµατος.
29
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Έτσι, τόσο το πρόβληµα των πολλών χρηστών επιλύεται όπως και το πρόβληµα της
διόρθωσης των προγραµµάτων, αφού κάθε υπολογιστής αλληλεπιδρά µε το χρήστη.
Τέλος, δεν υπάρχει ανάγκη να γραφούν δύο διαφορετικά προγράµµατα για κάθε
εφαρµογή, αφού όλοι οι υπολογιστές τρέχουν το ίδιο πρόγραµµα.
Σχήµα 18. Αρχιτεκτονική ενός συµµετρικού συστήµατος πολλών υπολογιστών.
Το βασικό πρόβληµα αυτών των συστηµάτων είναι ότι δεν µπορεί να γίνει η βέλτιστη
κατανοµή φόρτου εργασίας σε κάθε υπολογιστή, αφού αυτό εξαρτάται από τον αριθµό
των χρηστών που είναι συνδεδεµένοι σε κάθε υπολογιστή.
∆ιαφορές µεταξύ cluster και δικτύων υπολογιστών
Όπως αναφέραµε, ένας cluster χτίζεται από υλικά που χρησιµοποιούνται για την
κατασκευή δικτύων υπολογιστών. Οι βασικές διαφορές των δύο συστηµάτων είναι:
1.
Ο βασικός ρόλος ενός σταθµού εργασίας σε ένα δίκτυο υπολογιστών
είναι να εξυπηρετήσει τις ανάγκες του χρήστη του. Επίσης κάθε
σταθµός εργασίας µπορεί να έχει το δικό του λειτουργικό σύστηµα. Σε
έναν cluster όµως οι υπολογιστές πολλές φορές δε χρειάζεται να έχουν
user interface και όλοι τρέχουν το ίδιο λειτουργικό σύστηµα.
2.
Σε έναν cluster η επικοινωνία γίνεται µε διακόπτες και όχι µε.
Καθυστέρηση
Εύρος
Κόστος
Γρήγορο Ethernet
100µsec
100 Mbit/sec
100€
Gigabit Ethernet
100µsec
1000 Mbit/sec
1000€
7µsec
1920 Mbit/sec
2000€
Myrinet
Πίνακας 1. Οι τρεις επιλογές για το κτίσιµο του επικοινωνιακού στρώµατος ενός cluster.
30
∆.Σ. Βλάχος και Θ.Η. Σίµος
Η ταξινόµηση του Flynn
Η καλύτερη σήµερα γνωστή ταξινόµηση των υπολογιστικών συστηµάτων οφείλεται στον
Flynn. Σύµφωνα µε αυτήν, τα υπολογιστικά συστήµατα κατατάσσονται σε τέσσερις
κατηγορίες ανάλογα µε τον τρόπο που εκτελούν τις εντολές και τον τρόπο που
διαχειρίζονται τα δεδοµένα. Οι κατηγορίες αυτές είναι:
SISD (Single Instruction Single Data). Στα συστήµατα αυτά κάθε στιγµή εκτελείται
µία εντολή σε ένα τµήµα δεδοµένων. Οι κλασσικοί υπολογιστές ανήκουν σε αυτήν την
κατηγορία, αν και κάποιοι από αυτούς σήµερα µπορούν να εκτελέσουν παράλληλα
κάποιες λειτουργίες (πχ. Η ανάκληση εντολών στους PENTIUM).
SIMD (Single Instruction Multiple Data). Στα συστήµατα αυτά κάθε στιγµή εκτελείται
µια εντολή σε πολλές οµάδες δεδοµένων. Στην κατηγορία αυτήν ανήκουν οι µήτρες
επεξεργαστών.
MISD (Multiple Instruction Single Data). Στα συστήµατα αυτά κάθε στιγµή
εκτελούνται πολλές εντολές σε ένα τµήµα δεδοµένων. Τα συστήµατα αυτά δεν είναι
ιδιαίτερα δηµοφιλή και τα συναντά κανείς σε ειδικές εφαρµογές.
MIMD (Multiple Instruction Multiple Data). Στα συστήµατα αυτά κάθε στιγµή
εκτελούνται πολλές εντολές σε πολλές οµάδες δεδοµένων. Τα περισσότερα σύγχρονα
παράλληλα συστήµατα ανήκουν σε αυτήν την κατηγορία.
Σχήµα 19. Η ταξινόµηση του Flynn.
31
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
32
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχεδίαση παράλληλων
αλγορίθµων
Εισαγωγή
Στο σηµείο αυτό µπορούµε να ξεκινήσουµε να σχεδιάζουµε παράλληλους αλγόριθµους.
Η µεθοδολογία που θα υιοθετήσουµε περιγράφηκε από τον Foster και βασίζεται στο
λεγόµενο µοντέλο εργασία/ κανάλι. Το µοντέλο αυτό διευκολύνει την ανάπτυξη
αποδοτικών παράλληλων αλγόριθµων και ιδιαίτερα αυτών που πρόκειται να τρέξουν σε
συστήµατα µε κατανεµηµένη µνήµη.
Αρχικά θα αναπτύξουµε το µοντέλο εργασία/ κανάλι και τα βασικά βήµατα που πρέπει ν’
ακολουθήσει κανείς για να σχεδιάσει έναν παράλληλο αλγόριθµο. Στη συνέχεια θα
µελετήσουµε κάποια απλά προβλήµατα. Για κάθε ένα από αυτά θα σχεδιάσουµε έναν
παράλληλο αλγόριθµο που βασίζεται στο µοντέλο εργασία/ κανάλι και θα εκτιµήσουµε
τον υπολογιστικό χρόνο.
Το µοντέλο εργασία/ κανάλι.
Το µοντέλο εργασία/ κανάλι θεωρεί τον παράλληλο υπολογισµό σαν ένα σύνολο από
αυτόνοµες εργασίες που µπορούν να αλληλεπιδρούν µεταξύ τους στέλνοντας µηνύµατα
µέσω καναλιών σύνδεσης. Μια εργασία είναι ένα πρόγραµµα µε την τοπική του µνήµη
και τις πόρτες εισόδου εξόδου δεδοµένων. Η τοπική µνήµη περιέχει τις εντολές του
προγράµµατος και τα ιδιωτικά δεδοµένα του προγράµµατος. Μια εργασία στέλνει τα
περιεχόµενα των τοπικών της µεταβλητών από τις πόρτες εξόδου. Αντίστροφα, µια
εργασία µπορεί να λάβει δεδοµένα από τις πόρτες εισόδου. Ένα κανάλι είναι µια σειρά
(ουρά-queue) µηνυµάτων η οποία συνδέει την πόρτα εξόδου µιας εργασίας µε την πόρτα
εισόδου µιας άλλης εργασίας. Τα δεδοµένα εµφανίζονται στην έξοδο του καναλιού µε
την ίδια σειρά µε την οποία µπήκαν στην είσοδο του καναλιού.(FIFO).
Προφανώς µια εργασία δεν µπορεί να λάβει δεδοµένα από ένα κανάλι αν η εργασία που
βρίσκεται συνδεδεµένη στο άλλο άκρο του καναλιού δεν έχει στείλει δεδοµένα. Αν µια
εργασία προσπαθήσει να λάβει δεδοµένα από ένα κανάλι το οποίο δεν έχει διαθέσιµα
δεδοµένα , τότε η εργασία µπλοκάρεται. Αντίθετα µια εργασία η οποία στέλνει δεδοµένα
ποτέ δεν µπορεί να µπλοκαριστεί. Με άλλα λόγια η µετάδοση δεδοµένων είναι µια
ασύγχρονη διαδικασία ενώ η λήψη δεδοµένων είναι µια σύγχρονη διαδικασία.
Στο µοντέλο εργασία /κανάλι η προσπέλαση των τοπικών δεδοµένων διαχωρίζεται από
αυτή που γίνεται µέσω καναλιών. Πρέπει να έχουµε στο νου µας ότι η προσπέλαση των
τοπικών δεδοµένων είναι σαφώς πιο γρήγορη από αυτή µέσω καναλιών.
33
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Τέλος µε τον όρο χρόνο εκτέλεσης ενός παράλληλου αλγόριθµου εννοούµε το χρόνο που
µεσολαβεί από τη στιγµή που εκκινήθηκε η πρώτη εργασία µέχρι τη στιγµή που
τερµατίστηκε και η τελευταία.
Σχήµα 1. Το µοντέλο εργασία/ κανάλι. (a) Μια εργασία αποτελείται από ένα πρόγραµµα,
µια τοπική µνήµη και ένα σύνολο από κανάλια εισόδου και εξόδου. (b) Ένας παράλληλος
υπολογισµός µπορεί να θεωρηθεί σαν ένας προσανατολισµένος γράφος του οποίου οι
κόµβοι είναι οι εργασίες και οι ακµές τα κανάλια επικοινωνίας.
Η µεθοδολογία σχεδίασης του Foster.
Ο Foster πρότεινε µια διαδικασία που βασίζεται σε τέσσερα βήµατα για τη σχεδίαση
παράλληλων αλγόριθµων. Η διαδικασία αυτή επιτρέπει την ανάπτυξη αλγορίθµων
κλίµακας και αφήνει τις λεπτοµέρειες υλοποίησης για το τέλος. Τα τέσσερα βήµατα είναι
ο τεµαχισµός, η επικοινωνία, η συσσώρευση και η απεικόνιση (σχήµα 2).
Σχήµα 2. Η µεθοδολογία σχεδίασης παράλληλου αλγόριθµου του Foster.
34
∆.Σ. Βλάχος και Θ.Η. Σίµος
Τεµαχισµός
Το πρώτο µέληµα για τη σχεδίαση ενός καλού παράλληλου αλγόριθµου είναι η εύρεση
όσο το δυνατόν περισσότερων δυνατοτήτων παραλληλισµού. Ο τεµαχισµός είναι η
διαδικασία κατά την οποία διαιρείται ο υπολογισµός των δεδοµένων σε τµήµατα. Ένας
καλός τεµαχισµός διαιρεί και τους υπολογισµούς και τα δεδοµένα σε µικρά τµήµατα. Για
να επιτευχθεί αυτό µπορούµε να προσεγγίσουµε το πρόβληµα είτε από την πλευρά των
δεδοµένων είτε από την πλευρά των υπολογισµών.
Η διάσπαση του πεδίου ορισµού είναι ο παράλληλος αλγόριθµος που πρώτα διαιρεί τα
δεδοµένα σε τµήµατα και µετά αποφασίζει πως θα τα αντιστοιχίσει στους υπολογισµούς.
Θεωρείστε το παράδειγµα που φαίνεται στο σχήµα 3. Ας υποθέσουµε ότι το πεδίο
ορισµού των δεδοµένων αντιστοιχεί σ’ έναν τρισδιάστατο πίνακα. Θα µπορούσαµε να
τεµαχίσουµε τα δεδοµένα µας σε διδιάστατα επίπεδα προκύπτοντας έτσι µια
µονοδιάστατη συλλογή από εργασίες. Εναλλακτικά θα µπορούσαµε να τεµαχίσουµε τα
δεδοµένα µας σε µονοδιάστατες γραµµές παίρνοντας έτσι µια διδιάστατη συλλογή από
εργασίες. Τέλος θα µπορούσαµε να αντιστοιχίσουµε σε κάθε θέση του τρισδιάστατου
πίνακα µια εργασία παίρνοντας έτσι µια τρισδιάστατη συλλογή από εργασίες. Στο βήµα
του τεµαχισµού είναι καλύτερα να µεγιστοποιούµε τις εργασίες. Έτσι ο τρισδιάστατος
τεµαχισµός είναι βέλτιστος.
Σχήµα 3. Τρεις διαφορετικές διασπάσεις του πεδίου ορισµού των δεδοµένων.
Η συναρτησιακή διάσπαση είναι η συµπληρωµατική τεχνική στην οποία διαιρούµε τους
υπολογισµούς σε τµήµατα και µετά αποφασίζουµε πως θα συνδέσουµε τα δεδοµένα αυτά.
Πολύ συχνά η συναρτησιακή διάσπαση οδηγεί σε συλλογές εργασιών που εκτελούνται
παράλληλα µε pipelines.
Για παράδειγµα θεωρείστε ένα σύστηµα αναγνώρισης εικόνας. Οι εργασίες που πρέπει να
γίνουν είναι α)οµαλοποίηση της εικόνας β)εξαγωγή χαρακτηριστικών της εικόνας γ)
35
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
αναγνώριση αντικειµένων και δ) απεικόνιση. Η συναρτησιακή διάσπαση µπορεί να δώσει
ένα pipeline για την παράλληλη υλοποίηση των παραπάνω λειτουργιών (σχήµα 4).
Σχήµα 4. Συναρτησιακή διάσπαση του προβλήµατος της αναγνώρισης εικόνας.
Ανεξάρτητα από τον τύπο της διάσπασης που θα επιλέξουµε κάθε στοιχειώδης
διαδικασία που προκύπτει ονοµάζεται αρχέγονη εργασία. Ο σκοπός της διάσπασης είναι
να παράγει όσο το δυνατόν περισσότερες αρχέγονες εργασίες, γιατί ο µέγιστος αριθµός
αυτών των εργασιών αποτελεί ένα άνω όριο του παραλληλισµού που µπορεί να
επιτευχθεί. Παρακάτω αναφέρουµε µερικά κριτήρια για την εκτίµηση της ποιότητας της
διάσπασης.
1.
Οι αρχέγονες διαδικασίες πρέπει να είναι τουλάχιστον µια τάξη
µεγέθους παραπάνω από τους διαθέσιµους επεξεργαστές.
2.
Οι υπολογισµοί και τα δεδοµένα που υπολείπονται πρέπει να
ελαχιστοποιηθούν
3.
Οι αρχέγονες εργασίες πρέπει να είναι του ιδίου µεγέθους
4.
Ο αριθµός των αρχέγονων εργασιών πρέπει να είναι αύξουσα
συνάρτηση του µεγέθους του προβλήµατος
Επικοινωνία
Από τη στιγµή που έχουµε προσδιορίσει τις αρχέγονες εργασίες, πρέπει να αποφασίσουµε
για το πως θα επικοινωνούν µεταξύ τους. Οι παράλληλοι αλγόριθµοι έχουν δύο ειδών
επικοινωνιακούς τύπους : τους τοπικούς και τους γενικούς. Όταν µια εργασία χρειάζεται
δεδοµένα από ένα µικρό αριθµό άλλων εργασιών για να κάνει έναν υπολογισµό λέµε ότι
έχουµε τοπική επικοινωνία. Αντίθετα, όταν χρησιµοποιούµε τα δεδοµένα ενός
σηµαντικού αριθµού εργασιών για να κάνουµε έναν υπολογισµό, λέµε ότι έχουµε γενική
επικοινωνία.
Η επικοινωνία µεταξύ των εργασιών προσδίδει έναν επιπλέον χρόνο στο συνολικό χρόνο
εκτέλεσης που δεν υπάρχει στο σειριακό αλγόριθµο. Ένα σύνολο κριτηρίων που µας
βοηθά να αξιολογήσουµε την επικοινωνιακή δοµή ενός παράλληλου αλγόριθµου είναι:
1.
οι επικοινωνίες πρέπει να είναι οµοιόµορφα κατανεµηµένες ανάµεσα
στις εργασίες
2.
Κάθε εργασία πρέπει να επικοινωνεί µε έναν µικρό αριθµό γειτονικών
εργασιών
3.
Οι εργασίες πρέπει να επικοινωνούν ταυτόχρονα
36
∆.Σ. Βλάχος και Θ.Η. Σίµος
Συσσώρευση
Το κύριο µέληµά µας στα δύο πρώτα βήµατα είναι από τη µια να µεγιστοποιήσουµε τον
αριθµό των αρχέγονων εργασιών και από την άλλη να ελαχιστοποιήσουµε την χρονική
καθυστέρηση που εισάγεται από τις επικοινωνίες µεταξύ των εργασιών. Στο σηµείο αυτό
πρέπει να επαναπροσδιορίσουµε τις παραµέτρους των παράλληλων αλγορίθµων ώστε να
µπορεί να τρέξει σε ένα πραγµατικό σύστηµα. Για παράδειγµα αν ο αριθµός των
αρχέγονων εργασιών είναι 10000 φορές µεγαλύτερος από τον αριθµό των διαθέσιµων
επεξεργαστών η δηµιουργία όλων αυτών των εργασιών θα είχε σηµαντικό κόστος στο
χρόνο εκτέλεσης. Πρέπει λοιπόν να συνδυάσουµε εργασίες σε µεγαλύτερες για να
µειώσουµε αυτό το κόστος.
Η συσσώρευση είναι η διαδικασία κατά την οποία οµαδοποιούµε εργασίες σε
µεγαλύτερες εργασίες µε σκοπό να βελτιώσουµε την απόδοση ή να απλοποιήσουµε τον
προγραµµατισµό. Μερικές φορές θέλουµε ο αριθµός των οµάδων εργασιών να είναι
µεγαλύτερος από τον αριθµό των επεξεργαστών. Άλλες πάλι συνήθως όταν
χρησιµοποιούµε ΜΡΙ, θέλουµε να είναι ίδιος µε τον αριθµό των επεξεργαστών. Ένας από
τους στόχους της συσσώρευσης είναι να ελαχιστοποιήσει το επικοινωνιακό κόστος. Αν
οµαδοποιήσουµε εργασίες που επικοινωνούν µεταξύ τους τότε ο χρόνος επικοινωνίας
µηδενίζεται. Με αυτόν τον τρόπο αυξάνουµε την τοπικότητα των παράλληλων
αλγορίθµων. Επίσης αν κάποιες εργασίες µπλοκάρονται επειδή περιµένουν την έξοδο
κάποιων άλλων εργασιών τότε είναι καλό αυτές οι εργασίες να οµαδοποιηθούν.
Ένας άλλος τρόπος να µειώσουµε το επικοινωνιακό κόστος είναι να οµαδοποιήσουµε
εργασίες που στέλνουν µε εργασίες που λαµβάνουν. (σχήµα 5).
Σχήµα 5. Οµαδοποίηση εργασιών για µείωση των καναλιών επικοινωνίας.
Ένας δεύτερος στόχος της συσσώρευσης είναι να διατηρεί ο αλγόριθµος την κλίµακά
του. Πρέπει να είµαστε σίγουροι ότι η σχεδίαση που υιοθετούµε θα είναι τέτοια που θα
της επιτρέπει να µεταφερθεί σε ένα σύστηµα µε περισσότερους επεξεργαστές. Για
παράδειγµα θεωρήστε ότι έχουµε να επεξεργαστούµε τα στοιχεία ενός πίνακα µε
διαστάσεις 8Χ128Χ256.Αν οργανώσουµε τα δεδοµένα στις δύο τελικές διαστάσεις και
έχουµε ένα σύστηµα µε 4 επεξεργαστές τότε κάθε επεξεργαστής θα είναι υπεύθυνος για
την επεξεργασία ενός 2Χ128Χ256 υποπίνακα. Η σχεδίαση αυτή µπορεί να µεταφερθεί σε
37
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
ένα σύστηµα µε 8 επεξεργαστές και τότε κάθε επεξεργαστής θα είναι υπεύθυνος για έναν
υποπίνακα µε διάσταση 1Χ128Χ256. ∆ιαπιστώνουµε όµως ότι η σχεδίαση δεν είναι
µεταφέρσιµη σένα σύστηµα µε περισσότερους από 8 επεξεργαστές. Έτσι η σχεδίαση που
επιτρέπει την καλύτερη κλιµάκωση είναι αυτή που οµαδοποιεί τα δεδοµένα ως προς τις
δύο πρώτες διαστάσεις (µπορεί να εφαρµοστεί σε σύστηµα µέχρι 256 επεξεργαστές).
Τέλος ένας ακόµα στόχος της συσσώρευσης είναι να µειώσει το χρόνο ανάπτυξης του
προγράµµατος. Η συσσώρευση µπορεί να γίνει µε τέτοιο τρόπο ώστε να
εκµεταλλευτούµε µε τον καλύτερο τρόπο το σειριακό κώδικα.
Παρακάτω αναφέρουµε ένα σύνολο από κριτήρια µε τα οποία µπορούµε να
αξιολογήσουµε τη συσσώρευση που υιοθετούµε:
1.
Η συσσώρευση πρέπει να αυξάνει την τοπικότητα των παράλληλων
αλγορίθµων
2.
∆ιπλοί υπολογισµοί χρειάζονται λιγότερο χρόνο από τις επικοινωνίες
που αντικαθιστούν
3.
Τα διπλά δεδοµένα πρέπει να είναι µικρά σε όγκο για να µπορεί η
σχεδίαση να κλιµακώνεται.
4.
Οι οµαδοποιηµένες εργασίες πρέπει να έχουν παρόµοιο υπολογιστικό
και επικοινωνιακό κόστος.
5.
Ο αριθµός των εργασιών είναι µια αύξουσα συνάρτηση του µεγέθους
του προβλήµατος
6.
Ο αριθµός των εργασιών είναι ο µικρότερος δυνατός αλλά µεγαλύτερος
από τον αριθµό των επεξεργαστών.
7.
Πρέπει να υπάρχει ισορροπία µεταξύ της συσσώρευσης και του
κέρδους που θα έχουµε από την χρήση του σειριακού κώδικα.
Απεικόνιση
Η απεικόνιση είναι η διαδικασία κατά την οποία οι διάφορες εργασίες κατανέµονται
στους επεξεργαστές. Ο στόχος της απεικόνισης είναι να µεγιστοποιήσουµε τη χρήση των
επεξεργαστών και να ελαχιστοποιήσουµε τις επικοινωνίες µεταξύ των επεξεργαστών. Η
χρήση των επεξεργαστών είναι η µέση τιµή των ποσοστών του χρόνου που οι
επεξεργαστές είναι ενεργοί. Η χρήση µεγιστοποιείται όταν οι υπολογισµοί κατανέµονται
οµοιόµορφα ανάµεσα στους επεξεργαστές. Αν κάποιος επεξεργαστής έχει µπλοκαριστεί
επειδή περιµένει δεδοµένα από κάποιον άλλο επεξεργαστή τότε η χρήση µειώνεται.
Η επικοινωνία µεταξύ των επεξεργαστών αυξάνεται όταν δύο εργασίες που
ανταλλάσσουν δεδοµένα έχουν αντιστοιχηθεί σε διαφορετικούς επεξεργαστές , ενώ
µειώνεται αν αυτές οι εργασίες αντιστοιχηθούν στον ίδιο επεξεργαστή.
Για παράδειγµα θεωρείστε την απεικόνιση που φαίνεται στο σχήµα 6. Οκτώ εργασίες
έχουν αντιστοιχηθεί σε τρεις επεξεργαστές : δύο εργασίες στον πρώτο και τρίτο και
τέσσερις εργασίες στον µεσαίο. Αν υποθέσουµε ότι κάθε επεξεργαστής έχει την ίδια
ταχύτητα και κάθε εργασία χρειάζεται τον ίδιο χρόνο τότε ο µεσαίος επεξεργαστής θα
καταναλώνει το διπλάσιο χρόνο από τους άλλους δύο. Επίσης, αν υποθέσουµε ότι κάθε
38
∆.Σ. Βλάχος και Θ.Η. Σίµος
επικοινωνία µεταφέρει τον ίδιο όγκο δεδοµένων τότε ο µεσαίος επεξεργαστής θα
καταναλώνει το διπλάσιο επικοινωνιακό χρόνο από τους άλλους δύο. Η αύξηση της
χρήσης των επεξεργαστών και η µείωση του επικοινωνιακού χρόνου είναι συνήθως σε
ανακολουθία. Για παράδειγµα αν έχουµε p επεξεργαστές και αντιστοιχίσουµε όλες τις
εργασίες στον ίδιο επεξεργαστή τότε µειώνουµε στο 0 τον επικοινωνιακό χρόνο αλλά και
τη χρήση στο 1/ p. Αντίστροφα αν µοιράσουµε µια εργασία σε κάθε επεξεργαστή
µεγιστοποιούµε τη χρήση στο 1 αλλά και τον επικοινωνιακό χρόνο. Η βέλτιστη λύση θα
βρίσκεται κάπου στη µέση.
Σχήµα 6. Κατά τη διαδικασία της απεικόνισης είναι καλύτερο τα κανάλια επικοινωνίας να
κατανέµονται οµοιόµορφα στους επεξεργαστές για καλύτερη χρήση τους.
Όταν ένα πρόβληµα τεµαχίζεται χρησιµοποιώντας τη διάσπαση του πεδίου ορισµού οι
εργασίες που προκύπτουν έχουν συνήθως παρόµοιο µέγεθος, πράγµα που σηµαίνει ότι το
υπολογιστικό φορτίο είναι οµοιόµορφα κατανεµηµένο ανάµεσα στις εργασίες. Τότε
µπορούµε να συσσωρεύσουµε τις εργασίες σε p οµάδες που ελαχιστοποιούν τον
επικοινωνιακό φόρτο και να απεικονίσουµε κάθε οµάδα σε έναν επεξεργαστή. Μερικές
φορές ο αριθµός των εργασιών είναι συγκεκριµένος αλλά κάθε εργασία χρειάζεται
διαφορετικό χρόνο. Τότε η κατανοµή πρέπει να γίνει µε τέτοιο τρόπο ώστε τελικά ο κάθε
επεξεργαστής να έχει παρόµοια χρήση.
Άλλες φορές πάλι η επικοινωνία µεταξύ των εργασιών είναι ακανόνιστη. Εδώ το κύριο
µέληµά µας πρέπει να είναι η µείωση του επικοινωνιακού φόρτου. Ας δούµε όµως και τι
γίνεται στην περίπτωση που ο αριθµός των εργασιών δεν είναι εκ των προτέρων γνωστός.
Στην περίπτωση αυτή ίσως είναι απαραίτητο να ενσωµατωθεί στο πρόγραµµά µας και µια
εργασία η οποία µε δυναµικό τρόπο θα κατανέµει τις άλλες εργασίες στους επεξεργαστές.
Τέλος σε πολλές περιπτώσεις δηµιουργούνται πολλές µικρές εργασίες που κάνουν µία
δουλειά και επιστρέφουν το αποτέλεσµά της .Εδώ µπορούµε να υιοθετήσουµε δύο
τεχνικές για την απεικόνιση.
Στην πρώτη τεχνική χωρίζουµε τους επεξεργαστές σε έναν ‘αφέντη’ και πολλούς
‘εργάτες’. Κάθε εργάτης ζητά από τον ‘αφέντη’ µια εργασία. Όταν τελειώσει ένας
39
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
εργάτης την εργασία του, στέλνει τα αποτελέσµατά του και ζητά µια νέα µέχρι η λίστα
των εργασιών που πρέπει να εκτελεστούν αδειάσει.
Η άλλη τεχνική είναι να µοιράζονται οι εργασίες στους επεξεργαστές. Όταν ένας
επεξεργαστής έχει µεγάλο φόρτο, ρωτά τους γειτονικούς του αν µπορεί να τους αναθέσει
µια εργασία.
Αν ένας επεξεργαστής δεν έχει κάποια εργασία τότε ζητά από τους γειτονικούς του να
του αναθέσουν κάποια από τις δικές τους. Συνδυασµοί επίσης των δύο εργασιών είναι
δυνατόν να υπάρξουν . Το σχήµα 7 παρουσιάζει συγκεντρωτικά τις διάφορες τεχνικές
απεικόνισης.
Σχήµα 7. ∆έντρο επιλογής τεχνικής απεικόνισης.
Τα κριτήρια για την αξιολόγηση της απεικόνισης είναι:
1.
Υπάρχουν πολλές εργασίες ανά επεξεργαστή.
2.
Υπάρχει και στατική και δυναµική απεικόνιση
3.
Στην δυναµική απεικόνιση οι ‘αφέντες’ δεν αποτελούν τροχοπέδη
4.
Στην στατική απεικόνιση ο λόγος εργασίας ανά επεξεργαστή είναι
τουλάχιστον 10:1
Πρόβληµα συνοριακών τιµών
Το πρώτο παράδειγµα στο οποίο θα εφαρµόσουµε τη µεθοδολογία σχεδίασης που
αναπτύξαµε αφορά ένα απλό αλλά ρεαλιστικό πρόβληµα. Όπως φαίνεται στο σχήµα 8,
µια λεπτή ράβδος από οµοιόµορφο υλικό πλαισιώνεται από ένα ιδανικό µονωτικό υλικό
που εµποδίζει τη µεταφορά θερµότητας από και προς τη ράβδο. Το µήκος της ράβδου
είναι 1 µέτρο. Και τα δύο άκρα της ράβδου είναι βυθισµένα σε πάγο 0οC ενώ η αρχική
θερµοκρασία της ράβδου στο σηµείο Χ είναι 100.sin(π-x).
40
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 8. Μια λεπτή ράβδος είναι τυλιγµένη µε ένα µονωτικό υλικό και τα άκρα της
βρίσκονται βυθισµένα σε πάγο.
Με την πάροδο του χρόνου η ράβδος ψύχεται. Μια µερική διαφορική εξίσωση
περιγράφει τη µεταβολή της θερµοκρασίας σε κάθε σηµείο της ράβδου σαν συνάρτηση
του χρόνου. Η µέθοδος των πεπερασµένων διαφορών είναι µια τεχνική για την
αριθµητική λύση µιας µερικής διαφορικής εξίσωσης. Στο σχήµα 9 φαίνονται τα
αποτελέσµατα µιας τέτοιας µεθόδου. Οι καµπύλες αφορούν τη θερµοκρασιακή βαθµίδα
κατά µήκος της ράβδου σε διάφορες χρονικές στιγµές. Όπως φαίνεται στο σχήµα η
θερµοκρασία πέφτει µε την πάροδο του χρόνου. Αν παρατηρήσει κανείς προσεκτικά θα
δει ότι οι καµπύλες αποτελούνται από 10 ευθύγραµµα τµήµατα.
Σχήµα 9. Η ράβδος ψύχεται µε την πάροδο του χρόνου. Εδώ φαίνεται η θερµοκρασία της
ράβδου για διάφορες χρονικές στιγµές.
Στην πραγµατικότητα η λύση είναι µια λεία καµπύλη αλλά η αριθµητική λύση είναι µία
προσέγγιση. Η µέθοδος των πεπερασµένων διαφορών που χρησιµοποιείται αποθηκεύει τη
θερµοκρασία σε έναν διδιάστατο πίνακα. Κάθε γραµµή του πίνακα περιλαµβάνει την
κατανοµή της θερµοκρασίας κατά µήκος της ράβδου µια δεδοµένη χρονική στιγµή. Η
ράβδος χωρίζεται σε n τµήµατα µήκους h έτσι κάθε γραµµή του πίνακα έχει n+1
στοιχεία. Αύξηση του n σηµαίνει µείωση του λάθους της προσέγγισης. Ο χρόνος από τη
στιγµή 0 έως Τ χωρίζεται σε m τµήµατα έτσι ο πίνακας έχει m+1 στοιχεία. Οι τιµές της
πρώτης γραµµής του πίνακα, που αντιστοιχούν στο χρόνο 0, είναι γνωστές( 0οC), ενώ ui,j
είναι η θερµοκρασία της ράβδου στο σηµείο i τη χρονική στιγµή j.Ο τύπος που
χρησιµοποιείται είναι:
ui , j +1 = rui −1, j + (1 − 2r )ui , j + rui +1, j
41
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
όπου r=k/h2.
Τεµαχισµός
Εφαρµόζουµε διάσπαση του πεδίου ορισµού µας και αντιστοιχίζουµε µια αρχέγονη
εργασία σε κάθε σηµείο του πλέγµατος. Έχουµε έτσι µια διδιάστατη δοµή για τις
εργασίες µας.
Επικοινωνία
Για κάθε ζευγάρι αρχέγονων εργασιών που η µια απαιτεί δεδοµένα από την άλλη
σχεδιάζουµε έναν σύνδεσµο. Έτσι η εργασία ui,j+1 συνδέεται µε τις ui-1,j, ui,j, ui+1,j. Έτσι,
κάθε εργασία έχει τρεις συνδέσµους προς τα έξω και τρεις προς τα µέσα. Το αποτέλεσµα
φαίνεται στο σχήµα 11α
Σχήµα 11. Γράφος εργασιών και καναλιών για το πρόβληµα των συνοριακών τιµών. Στο
(b) και (c) φαίνονται τροποποιηµένοι οι γράφοι µετά τη συσσώρευση.
Συσσώρευση και απεικόνιση
Συσσωρεύουµε τις εργασίες κατά στήλες. Αν αντιστοιχίζουµε κάθε εργασία σε έναν
επεξεργαστή τότε θα έχουµε χαµηλή χρήση των επεξεργαστών αφού ο υπολογισµός της
θερµοκρασίας το χρόνο t χρειάζεται τη γνώση της θερµοκρασίας το χρόνο t-1. Τα
αποτελέσµατα φαίνονται στο σχήµα 11b και είναι σαφώς πιο απλό. Κάθε οµαδοποιηµένη
εργασία αφορά στη θερµοκρασία σε ένα σηµείο της ράβδου και έχει δύο εισόδους και
δύο εξόδους.
Η συσσώρευση βέβαια µπορεί να είναι ακόµα µεγαλύτερη όταν έχουµε χωρίσει τη ράβδο
σε πολλά τµήµατα ( το n πολύ µεγάλο). Τότε οµαδοποιούµε γειτονικά τµήµατα όπως
φαίνεται στο σχήµα 11c.
Ανάλυση
Ας υποθέσουµε ότι χ είναι ο χρόνος που χρειάζεται για τον υπολογισµό του ui,j+1, όταν
έχουν δοθεί τα ui-1,j, ui,j, ui+1,j. Για να ενηµερωθούν τα n-1 εσωτερικά σηµεία της
διακριτοποίησης απαιτείται χρόνος (n-1)χ. ∆εδοµένου ότι έχουµε και m βήµατα χρόνου,
ο αλγόριθµος διαρκεί (n-1)χm.
Ας δούµε τώρα το χρόνο που διαρκεί ο παράλληλος αλγόριθµος. Ας υποθέσουµε ότι
έχουµε p επεξεργαστές. Τότε ο χρόνος για τον υπολογισµό του τµήµατος της ράβδου που
αντιστοιχεί σε έναν επεξεργαστή είναι [(n-1)/p].χ. Στον παράλληλο αλγόριθµο όµως
έχουµε και το χρόνο που δαπανούν οι επικοινωνίες. Ο κάθε επεξεργαστής θα δεχτεί δύο
φορές δεδοµένα από τους δύο γείτονές του και θα στείλει δύο φορές δεδοµένα στους δύο
γείτονές του. Έστω λ ο χρόνος που διαρκεί η µετάδοση ή παραλαβή των δεδοµένων. Αν
42
∆.Σ. Βλάχος και Θ.Η. Σίµος
υποθέσουµε ότι η αποστολή ενός µηνύµατος µε την παραλαβή µπορούν να γίνουν
ταυτόχρονα ο επεξεργαστής θα δαπανήσει χρόνο στις επικοινωνίες ίσο µε 2λ.έτσι ο
συνολικός χρόνος θα είναι: m[ x(n-1)/p +2λ]
Αναγωγή
Με δεδοµένο ένα σύνολο τιµών α0,α1,...,αν-1 και µια διµελή σχέση ⊗, αναγωγή των
στοιχείων αi ονοµάζεται η πράξη α0⊗α1⊗...⊗αν-1. Η πρόσθεση ν-στοιχείων είναι ένα
παράδειγµα αναγωγής. Με την ίδια λογική, η εύρεση του µεγίστου ή ελαχίστου από νστοιχεία είναι µια αναγωγή, παρ’ όλο που στις περισσότερες γλώσσες προγραµµατισµού
δεν υπάρχει ορισµένη κάποια διµελής σχέση που να βρίσκει το µέγιστο ή το ελάχιστο δύο
στοιχείων.
Είναι σαφές πως η αναγωγή n-στοιχείων απαιτεί (n-1) πράξεις, έχει δηλαδή
πολυπλοκότητα Θ(n). Το πρόβληµα µε το οποίο θα ασχοληθούµε στη συνέχεια είναι να
βρούµε έναν παράλληλο αλγόριθµο για τον υπολογισµό της αναγωγής n-στοιχείων,
Τεµαχισµός
Μπορούµε να υιοθετήσουµε το βέλτιστο τεµαχισµό, αυτόν δηλαδή που αντιστοιχεί σε
κάθε αρχέγονη εργασία έναν από τους n-αριθµούς. Έχουµε λοιπόν n-αρχέγονες εργασίες.
Επικοινωνία
Ο υπολογισµός της αναγωγής απαιτεί την επαναληπτική εφαρµογή της διµελούς σχέσης.
Αν µια εργασία είναι υπεύθυνη για την υλοποίηση της διµελούς σχέσης, πρέπει οι δύο
αριθµοί στους οποίους θα εφαρµοστεί η σχέση να µεταδοθούν στην εργασία αυτή.
Ας ξεκινήσουµε µε την πιο απλή περίπτωση. Ας υποθέσουµε ότι υπάρχει µια κύρια
εργασία η οποία συλλέγει τους αριθµούς από τις n-αρχέγονες εργασίες και υλοποιεί την
αναγωγή.
Σχήµα 12. Ένας αποδοτικός παράλληλος αλγόριθµος για αναγωγή.
43
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Αν κάθε επικοινωνία έχει χρόνο λ και ο υπολογισµός µιας πράξης χρόνο χ, ο συνολικός
χρόνος για την αναγωγή θα είναι (n-1)(λ+χ). Ο χρόνος αυτός είναι σαφώς µεγαλύτερος
από το χρόνο που διαρκεί ο σειριακός αλγόριθµος.
Στο επόµενο βήµα χωρίζουµε τις εργασίες σε δύο ίσα υποσύνολα, και εφαρµόζουµε την
τεχνική που αναφέραµε. Κάθε υποσύνολο έχει τώρα n/2 στοιχεία, και αφού ο
υπολογισµός της αναγωγής σε αυτά τα υποσύνολα θα γίνει ταυτόχρονα, θα διαρκέσει (n/2
–1)(λ+χ). Στο χρόνο αυτό πρέπει να προσθέσουµε και (λ+χ), αφού θα γίνει µια επιπλέον
επικοινωνία και πράξη για τον υπολογισµό του τελικού αποτελέσµατος. Ο συνολικός
χρόνος θα είναι n/2(λ+χ), σχεδόν δηλαδή ο µισός απ’ ότι στην πρώτη περίπτωση.
Συνεχίζοντας τη διαδικασία µπορούµε να χωρίσουµε τα δεδοµένα σε τέσσερα
υποσύνολα. Αυτό µπορεί να θεωρηθεί ότι εφαρµόζουµε παράλληλα σε δύο σύνολα
δεδοµένων την προηγούµενη διαδικασία. Έτσι ο χρόνος προκύπτει εύκολα αν στον
προηγούµενο χρόνο βάλουµε όπου n το n/2 και προσθέσουµε και (λ+χ) για τη µετάδοση
και πράξη που απαιτείται για τον υπολογισµό του τελικού αποτελέσµατος. Ο συνολικός
χρόνος λοιπόν θα είναι n/4(λ+χ)+(λ+χ).
Γενικεύοντας, µπορούµε να χωρίσουµε τα δεδοµένα µας σε 2p σύνολα, που το καθένα
από αυτά έχει n/2p στοιχεία. Ας ονοµάσουµε την τεχνική µας p-βαθµού. Το συµπέρασµα
που εξάγουµε είναι ότι:
t p ( n) = t p −1 ( n ) + λ + χ
2
δηλαδή ο χρόνος που διαρκεί η τεχνική p-βαθµού όταν εφαρµόζεται σ n στοιχεία είναι
ίσος µε το χρόνο που διαρκεί η τεχνική (p-1)-βαθµού όταν εφαρµόζεται σε n/2 στοιχεία
συν το χρόνο µιας µετάδοσης και µιας πράξης για την εύρεση του τελικού
αποτελέσµατος. Επειδή:
t p −1 ( n ) = t p − 2 ( n ) + λ + χ
2
4
προκύπτει:
t p ( n ) = t0 ( n
2p
) + p ⋅ (λ + χ )
Όµως το t0 (p=0 δηλαδή 20=1 σύνολο) το έχουµε ήδη υπολογίσει. Έτσι:
 n

t p ( n ) =  p − 1 ⋅ ( λ + χ ) + p ⋅ ( λ + χ ) ⇒
2

n
t p (n) = p ⋅ (λ + χ ) + ( p − 1) ⋅ (λ + χ )
2
Η τεχνική που αναπτύξαµε περιγράφεται από το διωνυµικό δέντρο (σχήµα 13).
44
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 13. ∆ιωνυµικά δέντρα µε 1, 2, 4 και 8 κόµβους.
Ένα διωνυµικό δέντρο έχει n=2k κόµβους και την εξής ιδιότητα: Το διωνυµικό δέντρο µε
n=2k+1 κόµβους προκύπτει αν πάρουµε δύο διωνυµικά δέντρα µε n=2k κόµβους και
ενώσουµε τις ρίζες τους. Μπορεί κανείς εύκολα να διαπιστώσει την αντιστοιχία ενός
διωνυµικού δέντρου µε την τεχνική που αναπτύξαµε. Στο σχήµα 14 φαίνεται η αναγωγή
16 στοιχείων (πρόσθεση) µε ένα διωνυµικό δέντρο.
Σχήµα 14. Υλοποίηση της αναγωγής µε τη χρήση ενός δυωνιµικού δέντρου. Η πράξη της
αναγωγής εδώ είναι η πρόσθεση.
Τι γίνεται όµως στην περίπτωση που το n δεν είναι δύναµη του 2; Τότε, βρίσκουµε το
µέγιστο k για το οποίο ισχύει: n=2k+r. Στο πρώτο βήµα, r-εργασίες στέλνουν τις τιµές
τους και r-εργασίες λαµβάνουν τις τιµές αυτές. Οι r-εργασίες που έστειλαν τις τιµές
απενεργοποιούνται, και µένουµε τελικά µε 2k εργασίες στις οποίες µπορούµε τώρα να
εφαρµόσουµε την τεχνική µας (σχήµα 15).
Συσσώρευση
Έστω ότι έχουµε p-επεξεργαστές, όπου το p είναι µια δύναµη του 2 αλλά πολύ µικρότερο
του n. Το κριτήριο για καλή συσσώρευση είναι να ελαχιστοποιήσουµε τις επικοινωνίες
µεταξύ των επεξεργαστών. Αν θυµηθούµε τη δοµή του διωνυµικού δέντρου, θα δούµε ότι
αυτό είναι µια σύνθεση πολλών υποδέντρων. Έτσι, η συσσώρευση γίνεται βέλτιστη αν
απεικονίσουµε σε κάθε επεξεργαστή ένα υποδέντρο του διωνυµικού δέντρου (σχήµα 16).
45
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Αν n=2d και p=2k, τότε το δυωνυµικό δέντρο αποτελείται από 2κ υποδέντρα, κάθε ένα
από τα οποία έχει 2d-k κόµβους. Έτσι, κάθε επεξεργαστής θα έχει n/p=2d-k στοιχεία να
επεξεργαστεί.
Ανάλυση
Στην έκφραση του χρόνου που υπολογίσαµε πριν, πρέπει να παρατηρήσουµε ότι σε κάθε
επεξεργστή οι χρόνοι επικοινωνίας είναι µηδέν. Έτσι, αν αντικαταστήσουµε το p µε logp
θα έχουµε για το συνολικό χρόνο:
t =(
n
− 1) ⋅ χ + log p ⋅ (λ + χ )
p
Σχήµα 15. Αναγωγή όταν ο αριθµός των εργασιών δεν είναι δύναµη του 2.
Σχήµα 16.Παράδειγµα συσσώρευσης. Σε κάθε οµάδα συσσωρεύονται οι κόµβοι ενός
υποδέντρου του δυωνυµικού δέντρου για να ελαχιστοποιηθούν οι σύνδεσµοι µεταξύ των
οµάδων.
Το πρόβληµα των n-σωµάτων
Το πρόβληµα των n-σωµάτων συναντάται πολύ συχνά στη φυσική και αφορά σε ένα
δυναµικό σύστηµα n-αλληλεπιδρόντων σωµάτων. Για παράδειγµα, στη µοριακή φυσική
οι δυνάµεις Coulomb µεταξύ των ατόµων είναι αυτές που καθορίζουν την κίνησή τους ή
στη µηχανική διαστήµατος οι θέσεις των πλανητών εξαρτώνται από τις βαρυτικές
δυνάµεις που αναπτύσσονται µεταξύ των πλανητών.
Στο σηµείο αυτό θα αναζητήσουµε ένα παράλληλο αλγόριθµο για τη λύση του
προβλήµατος. Θα θεωρήσουµε ότι τα σώµατα κινούνται σε δύο διαστάσεις. Σηµειώστε
εδώ ότι η πολυπλοκότητα του σειριακού αλγόριθµου είναι Θ(n2).
46
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 17. Στο πρόβληµα των n-σωµάτων, κάθε σώµα δέχεται δυνάµεις από τα υπόλοιπα
που του καθορίζουν τον τρόπο µε τον οποίο θα κινηθεί.
Τεµαχισµός
Ένας φυσιολογικός τεµαχισµός αντιστοιχίζει ένα σώµα σε µια αρχέγονη εργασία. Ο
υπολογισµός της νέας θέσης του σώµατος απαιτεί τη γνώση των θέσεων όλων των άλλων
σωµάτων.
Επικοινωνία
Υπάρχει µια µεγάλη οµάδα προβληµάτων που απαιτεί µια ειδική µορφή επικοινωνίας που
ονοµάζεται gather (συσσώρευση). Εδώ τα δεδοµένα από όλους τους επεξεργαστές πρέπει
να συλλεχθούν. Επίσης, µια all-gather επικοινωνία είναι σαν την gather µε τη διαφορά ότι
στο τέλος όλοι οι επεξεργαστές έχουν ένα αντίγραφο των δεδοµένων που συλλέχθηκαν.
Σχήµα 18. Αναπαράσταση της gather και all-gather επικοινωνίας.
Μια λύση είναι να έχουµε ένα σύνδεσµο ανάµεσα σε κάθε ζευγάρι εργασιών. Μετά από
κάθε υπολογισµό, κάθε εργασία στέλνει τα αποτελέσµατά της σε όλες τις άλλες εργασίες,
έχοντας έτσι συνολικά (n-1) παράλληλες επικοινωνίες (σχήµα 19).
47
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 19. Ένας τρόπος για την υλοποίηση της gather επικοινωνίας είναι να θεσπίσουµε
µια σύνδεση ανάµεσα σε κάθε ζευγάρι εργασιών.
Μπορούµε όµως να αναζητήσουµε µια τεχνική που να µειώνει τον αριθµό των
µεταδόσεων. Αν έχουµε δύο εργασίες, η κάθε µία στέλνει την τιµή της στην άλλη. Αν
έχουµε τέσσερις εργασίες, σε ένα βήµα οι εργασίες 0,1 και 2,3 ανταλλάσσουν τα
δεδοµένα τους. Στο επόµενο βήµα, η εργασία 0 ανταλλάσσει το ζευγάρι δεδοµένων που
έχει µε την εργασία 2 και η εργασία 1 µε την εργασία 3. Έτσι, όλες οι εργασίες έχουν όλα
τα δεδοµένα σε δύο βήµατα.
Γενικεύοντας αυτήν την τεχνική µπορούµε να υλοποιήσουµε µια all-gather επικοινωνία
µεταξύ n-εργασιών σε logn επικοινωνιακά βήµατα. Η διαφορά εδώ όµως είναι ότι σε
κάθε βήµα ο όγκος των δεδοµένων που ανταλλάσσονται διπλασιάζεται. Έτσι, στο βήµα 0
θα έχουµε 20 δεδοµένα, στο βήµα 1 21 δεδοµένα και στο βήµα k 2k δεδοµένα.
Σχήµα 20. Βελτιωµένος τρόπος υλοποίησης της gather επικοινωνίας µε logp κανάλια
εισόδου και εξόδου.
Συσσώρευση και απεικόνιση
Γενικά θα έχουµε πολύ περισσότερα σώµατα από επεξεργαστές (n>>p). Τότε, µπορούµε
να αντιστοιχίσουµε n/p σώµατα σε κάθε επεξεργαστή. Έτσι, η επικοινωνία all-gather
απαιτεί logp βήµατα. Στο πρώτο βήµα θα ανταλλαγούν n/p δεδοµένα, στο δεύτερο 2n/p
δεδοµένα και στο k-βήµα 2kn/p δεδοµένα.
Ανάλυση
Επειδή εδώ τα µηνύµατα που ανταλλάσσονται έχουν διαφορετικό µέγεθος, θα πρέπει
στην ανάλυσή µας να θεωρήσουµε και αυτήν την παράµετρο. Ας υποθέσουµε ότι κάθε
µήνυµα καταναλώνει ένα σταθερό χρόνο λ ανεξάρτητα από το µήκος του. Στη συνέχεια,
αν το εύρος ζώνης του καναλιού σύνδεσης είναι β (το β είναι το πλήθος των δεδοµένων
που µπορούν να µεταδοθούν ανά µονάδα χρόνου), τότε ο χρόνος για τη µετάδοση ενός
µηνύµατος µήκους n θα είναι λ+n/β.
48
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 21. Ο χρόνος επικοινωνίας σαν συνάρτηση του όγκου δεδοµένων.
Ο χρόνος για την επικοινωνία σε κάθε επανάληψη θα είναι:
log p 
2i −1 n 
n( p − 1)
∑  λ + β p  = λ ⋅ log p + β p
i =1 

Σε αυτό το χρόνο πρέπει να προσθέσουµε και τον υπολογιστικό χρόνο. Αν η εύρεση της
θέσης ενός σώµατος απαιτεί χρόνο χ, κάθε επεξεργαστής υπολογίζει n/p σώµατα, άρα
χρειάζεται χρόνο nχ/p. Έτσι, ο συνολικός χρόνος θα είναι:
λ ⋅ log p +
n( p − 1)
n
+χ
βp
p
Είσοδος δεδοµένων
Ένα στοιχείο το οποίο δεν έχουµε εξετάσει είναι πως εισάγονται τα δεδοµένα του
προγράµµατος. Πολλοί παράλληλοι υπολογιστές σήµερα έχουν παράλληλα συστήµατα
εισόδου/εξόδου, αλλά οι εµπορικοί clusters αφήνουν αυτή τη δουλειά σε έναν κεντρικό
υπολογιστή. Εδώ θα θεωρήσουµε την είσοδο των αρχικών θέσεων και ταχυτήτων του
προβλήµατος των n-σωµάτων. Όπως έχουµε αναφέρει, τα σώµατα κινούνται σε δύο
διαστάσεις, εποµένως θέλουµε να εισάγουµε δύο τιµές για κάθε θέση και δύο για κάθε
ταχύτητα. Έτσι για τα n-σώµατα απαιτείται χρόνος
λio +
4n
β io
όπου ο δείκτης i δείχνει ότι αναφερόµαστε στο κανάλι εισόδου (σχήµα 22).
49
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 22. Ο γράφος εργασιών και καναλιών για την υλοποίηση της λειτουργίας εισόδου
των δεδοµένων.
Επικοινωνία
Η διαδικασία κατά την οποία τα δεδοµένα πρέπει να διασκορπιστούν ανάµεσα στους
επεξεργαστές ονοµάζεται σκέδαση (scatter). Μπορεί εύκολα να δει κανείς ότι η σκέδαση
είναι µια διαδικασία αντίστροφη της συλλογής (gather).
Ο πιο απλός τρόπος για να γίνει η σκέδαση των δεδοµένων είναι να αφήσουµε τον
επεξεργαστή που διαβάζει τα δεδοµένα να στείλει (p-1) µηνύµατα, κάθε ένα µήκους 4n/p,
στους υπόλοιπους επεξεργαστές. Ο χρόνος που απαιτείται τότε είναι:
( p − 1) ⋅ (λ +
4n
)
βp
Ας δούµε πως µπορούµε να µειώσουµε αυτόν το χρόνο, προσπαθώντας να τον κάνουµε
πάλι ανάλογο του logp. Ας υποθέσουµε ότι αρχικά ο επεξεργαστή που έχει τα δεδοµένα,
στέλνει τα µισά από αυτά σε έναν άλλο. Στη συνέχεια, οι δύο επεξεργαστές, που έχουν
από µισά δεδοµένα, στέλνουν τα µισά από αυτά που έχουν σε δύο άλλους επεξεργαστές.
Έτσι, θα έχουµε τέσσερις επεξεργαστές που θα έχουν από ένα τέταρτο των δεδοµένων.
Συνεχίζοντας έτσι, πετυχαίνουµε logp µηνύµατα. Ο χρόνος θα είναι:
log p
4n
∑ λ + 2i p β
i =1
= λ ⋅ log p + 4n
p −1
βp
Η µείωση του χρόνου εδώ βασίζεται στο γεγονός ότι, ενώ στον πρώτο αλγόριθµο, κάθε
φορά στέλναµε ένα µήνυµα από τον επεξεργαστή που έχει τα δεδοµένα σε κάποιον άλλο,
στη δεύτερη περίπτωση, ταυτόχρονα, πολλοί επεξεργαστές που έχουν δεδοµένα θα
στέλνουν τα µισά από αυτά που έχουν σε κάποιον άλλο.
Ανάλυση
Ας δούµε τώρα συνολικά το χρόνο που διαρκεί η εκτέλεση του αλγόριθµου που
αναπτύξαµε. Αρχικά η είσοδος και έξοδος των δεδοµένων µας κρατά χρόνο:
50
∆.Σ. Βλάχος και Θ.Η. Σίµος
2(λio +
4n
)
β io
Η σκέδαση αρχικά και η συλλογή των δεδοµένων στο τέλος του αλγόριθµου διαρκεί:
2(λ ⋅ log p + 4n
p −1
)
βp
Κάθε βήµα του αλγόριθµου απαιτεί επικοινωνία µεταξύ των επεξεργαστών που διαρκεί:
λ ⋅ log p + 2n
p −1
βp
και χρόνο για υπολογισµό:
χ⋅
n
⋅ (n − 1)
p
Έτσι, συνολικά ο αλγόριθµος διαρκεί (για m βήµατα του αλγόριθµου):
2(λio +
4n
β io
) + 2(λ ⋅ log p + 4n
p −1
p −1
n
) + m(λ ⋅ log p + 2n
+ χ (n − 1))
βp
βp
p
51
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
52
∆.Σ. Βλάχος και Θ.Η. Σίµος
Προγραµµατισµός µε
διαβίβαση µηνυµάτων
Εισαγωγή
Τα τελευταία 40 χρόνια έχουν προταθεί δεκάδες γλώσσες για παράλληλο
προγραµµατισµό. Πολλές από αυτές είναι υψηλού επιπέδου που διευκολύνουν τον
προγραµµατισµό στη διαδικασία της παραλληλοποίησης. Παρ’ όλ’ αυτά, καµία από
αυτές δεν έχει κερδίσει την εµπιστοσύνη των προγραµµατιστών. Έτσι, οι πιο πολλοί
προγραµµατιστές συνεχίζουν να χρησιµοποιούν την Fortran και τη C εµπλουτισµένες µε
δυνατότητα ανταλλαγής µηνυµάτων. Το MPI (Message Passing Interface) έχει γίνει
σήµερα ένα standard για την ανάπτυξη παράλληλων προγραµµάτων. Είναι πολύ εύκολο
να στήσει κανείς σήµερα µε ελεύθερο λογισµικό ένα cluster στο σπίτι του.
Στο σηµείο αυτό θα ξεκινήσουµε να προγραµµατίζουµε ένα παράλληλο υπολογιστή µε
χρήση της γλώσσας C και των εργαλείων που προσφέρει το MPI. Η στοιχειώδης γνώση
της γλώσσας C θεωρείται δεδοµένη. Οι λειτουργίες του MPI µε τις οποίες θα
ασχοληθούµε είναι:
MPI_Init
MPI_Comm_rank
MPI_Comm_size
MPI_Reduce
MPI_Finalize
MPI_Barrier
MPI_Wtime
MPI_Wtick
για την αρχικοποίηση του MPI
για να προσδιορίσει µια εργασία την ταυτότητά της
για να ανακτήσει κανείς τον αριθµό των εργασιών
για να κάνει κανείς µια λειτουργία αναγωγής
για να τερµατιστεί η λειτουργία του MPI
για να κάνει κανείς µια εργασία συγχρονισµού φράγµατος
για να υπολογίσει κανείς το χρόνο
για να βρει κανείς την ακρίβεια του χρονοµέτρου
Το µοντέλο διαβίβασης µηνυµάτων
Το µοντέλο διαβίβασης µηνυµάτων είναι παρόµοιο µε το µοντέλο εργασία-κανάλι που
αναπτύχθηκε στα προηγούµενα κεφάλαια. Το υλικό υποτίθεται πως είναι µια συλλογή
από υπολογιστές, κάθε ένας από τους οποίους υποτίθεται πως έχει τη δική του µνήµη.
Ένα δίκτυο όµως διασύνδεσης επιτρέπει την ανταλλαγή µηνυµάτων µεταξύ των
υπολογιστών. Έτσι ο υπολογιστής Α µπορεί να στείλει µε ένα µήνυµα το περιεχόµενο
53
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
µιας τοπικής του µεταβλητής στον υπολογιστή Β κάνοντας έτσι τον Β να έχει µια έµµεση
δυνατότητα πρόσβασης στη µνήµη του υπολογιστή Α.
Σχήµα 1. Το µοντέλο διαβίβασης µηνυµάτων υποθέτει πως το υλικό αποτελείται από ένα
σύνολο επεξεργαστών µε τις τοπικές τους µνήµες και ένα δίκτυο διασύνδεσης.
Μια εργασία στο µοντέλο εργασία/ κανάλι γίνεται µια διεργασία στο µοντέλο διαβίβασης
µηνυµάτων. Κάθε διεργασία µέσω του δικτύου διασύνδεσης µπορεί να ανταλλάξει
µηνύµατα µε τις άλλες διεργασίες. Οι τεχνικές µείωσης του επικοινωνιακού χρόνου που
αναπτύξαµε στα παραδείγµατα του προηγούµενου κεφαλαίου είναι και εδώ εφαρµόσιµες.
Ο χρήστης καθορίζει τον αριθµό των παράλληλων διεργασιών µε τη εκκίνηση του
προγράµµατος. Τυπικά ο αριθµός αυτός παραµένει σταθερός. Κάθε διεργασία τρέχει το
ίδιο πρόγραµµα, αλλά επειδή κάθε διεργασία έχει τη δική της ταυτότητα, οι διεργασίες
καταλήγουν να κάνουν διαφορετικές λειτουργίες καθώς το πρόγραµµα ξεδιπλώνεται . Οι
διεργασίες εναλλάσσουν τη λειτουργία τους ανάµεσα στην επεξεργασία των δικών τους
δεδοµένων και τη στην επικοινωνία µε άλλες διεργασίες.
Η ανταλλαγή µηνυµάτων πέρα από την ανταλλαγή των δεδοµένων, εξυπηρετεί και το
συγχρονισµό των διεργασιών. Έτσι όταν η διεργασία Α στέλνει ένα µήνυµα στη
διεργασία Β, η διεργασία Β αποκτά γνώση για την κατάσταση της Α ακόµα και αν το
µήνυµα είναι κενό. Οι υποστηρικτές του µοντέλου διαβίβασης µηνυµάτων καταδεικνύουν
πολλά προτερήµατα. Το πρώτο είναι ότι το µοντέλο αυτό είναι ιδανικό για συστήµατα
που δεν έχουν κοινή µνήµη. Αλλά ακόµα και στα συστήµατα µε µια κεντρική µνήµη, το
µοντέλο της διαβίβασης µηνυµάτων οδηγεί τους προγραµµατιστές να σχεδιάζουν
αλγόριθµους µε αυξηµένη τοπικότητα, πράγµα που τους κάνει να τρέχουν αποδοτικότερα
και για συστήµατα µε κεντρική µνήµη.
54
∆.Σ. Βλάχος και Θ.Η. Σίµος
Επιπλέον η διόρθωση των προγραµµάτων γίνεται µε πιο εύκολο τρόπο. Οι µεταβλητές
επειδή είναι τοπικές σε κάθε υπολογιστή, παρακολουθούνται πιο εύκολα από ότι αν ήταν
γενικές, οπότε ο ρυθµός και ο τρόπος µε τον οποίο αναφέρονται σ’ αυτές οι διάφορες
εργασίες είναι σχεδόν µια τυχαία διαδικασία.
Η διεπιφάνεια της διαβίβασης µηνυµάτων
ΤΟ 1992 δηµιουργήθηκε µια οµάδα ερευνητών από το Κέντρο Έρευνας στους
Παράλληλους Υπολογισµούς. Η οµάδα αυτή αποτελούνταν από εκπροσώπους και της
βιοµηχανίας και των Πανεπιστηµίων. Το Μάιο του 1994 εµφανίστηκε η πρώτη έκδοση
του MPI. Τον Απρίλιο του 1997 εµφανίστηκε το MPI-2. Σήµερα το MPI έχει γίνει το πιο
δηµοφιλές standard για τη διαβίβαση µηνυµάτων. ∆ιατίθεται ελεύθερα για όλες σχεδόν
τις εµπορικές πλατφόρµες και επιτρέπει στους χρήστες που το χρησιµοποιούν να
µεταφέρουν εύκολα τον κώδικά τους.
Εύρεση πίνακα αληθείας ψηφιακού κυκλώµατος
Για να εισάγουµε τη χρήση του MPI θα προσπαθήσουµε να γράψουµε ένα παράλληλο
πρόγραµµα για την εύρεση του πίνακα αληθείας ενός ψηφιακού κυκλώµατος. Το
κύκλωµα φαίνεται στο σχήµα 2 και έχει 16 εισόδους.
Σχήµα 2. Ένα ψηφιακό κύκλωµα του οποίου ζητάµε να βρούµε τον πίνακα αληθείας,
55
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Το πρόβληµα είναι NP- πλήρες, πράγµα που σηµαίνει ότι δεν έχει βρεθεί αλγόριθµος που
να το επιλύει σε πολυωνυµικό χρόνο. Στην περίπτωσή µας αφού έχουµε 16 εισόδους θα
έχουµε 216=65536 συνδυασµούς των εισόδων.
Μια φυσιολογική προσέγγιση στο πρόβληµα είναι να αντιστοιχήσουµε ένα συνδυασµό
εισόδων σε µια εργασία. Παρατηρούµε εδώ ότι έχουµε συναρτησιακό παραλληλισµό και
όχι παραλληλισµό δεδοµένων, γιατί κάθε εργασία εκτελεί µια διαφορετική λειτουργία.
Αν µια εργασία βρει ότι ο συνδυασµός που ελέγχει ικανοποιεί το κύκλωµα (έχει έξοδο 1),
αναφέρει αυτόν τον συνδυασµό. Ο γράφος εξάρτησης δεδοµένων φαίνεται στο σχήµα 3.
Σχήµα 3. Ο γράφος εργασιών και καναλιών για το πρόβληµα της εύρεσης του πίνακα
αληθείας του κυκλώµατος του σχήµατος 2.
Παρατηρείστε ότι δεν υπάρχει επικοινωνία µεταξύ των εργασιών. Υπάρχει όµως ένα
κανάλι (δε φαίνεται στο σχήµα 3) µεταξύ κάθε εργασίας και της συσκευής εξόδου για την
εκτύπωση των αποτελεσµάτων. Ας δούµε λίγο τώρα το θέµα της συσσώρευσης και της
απεικόνισης. Αν έχουµε p επεξεργαστές µπορούµε να αντιστοιχήσουµε 65536/ p
συνδυασµούς σε κάθε διεργασία. Ένα θέµα που προκύπτει είναι πως θα επιλέξουµε τους
65536 / p συνδυασµούς για κάθε επεξεργαστή. Μια λύση είναι να πάρουµε τους πρώτους
65536 / p και να τους αντιστοιχήσουµε στον πρώτο επεξεργαστή, τους δεύτερους 65536 /
p στον δεύτερο κ.ο.κ. Λαµβάνοντας όµως υπ’ όψιν ότι οι µοντέρνες γλώσσες
προγραµµατισµού όπως η C υλοποιούν τη λεγόµενη ‘καλωδιωµένη λογική’, κάθε
συνδυασµός δεν θα δαπανήσει τον ίδιο χρόνο για να ελεγχθεί. Έτσι αν η οµάδα των
συνδυασµών που θα υλοποιήσουµε σε έναν υπολογιστή έχει αυξηµένη τοπικότητα τότε
είναι πολύ πιθανό να έχουµε αυξηµένη χρήση σε κάθε υπολογιστή. Μια άλλη λογική µε
την οποία µπορούµε να καλύψουµε καλύτερα τον χώρο των δεδοµένων είναι η τεχνική
της διαφύλλωσης. Έστω ότι έχουµε η=20 συνδυασµούς και θέλουµε να τους µοιράσουµε
σε 6 επεξεργαστές. Μπορούµε στον πρώτο επεξεργαστή να αναθέσουµε τους
συνδυασµούς 0,6,12,18, στο δεύτερο τους 1,7,13,19 κ.ο.κ. Γενικά αν έχουµε η
συνδυασµούς και p επεξεργαστές αναθέτουµε σε κάθε επεξεργαστή τους συνδυασµούς
που το υπόλοιπο της διαίρεσής τους µε το p έχει το ίδιο υπόλοιπο (ισοδύναµο modulo p).
Οι πρώτες εντολές που θα γράψουµε στο πρόγραµµά µας είναι:
#include <mpi.h>
#include <stdio.h>
µε τις οποίες συνδεόµαστε µε τις επικεφαλίδες του MPI και της εισόδου- εξόδου. Στη
συνέχεια:
int main (int arg c, char
*argv[]){
56
∆.Σ. Βλάχος και Θ.Η. Σίµος
int i;
int id;
int p;
void check_circuit (int, int)
Η µεταβλητή i θα χρησιµοποιηθεί σαν µετρητής, η id θα αποθηκεύει την ταυτότητα της
κάθε εργασίας, η p τον αριθµό των διεργασιών και η συνάρτηση check_circuit είναι
εκείνη που θα ελέγξει την αλήθεια ενός συνδυασµού.
Η συνάρτηση MPI_Init
Είναι η πρώτη συνάρτηση που πρέπει να κληθεί πριν από οποιαδήποτε άλλη συνάρτηση
MPI και αρχικοποιεί το περιβάλλον του MPI.
ΟΙ συναρτήσεις MPI_Comm_rank και MPI_Comm_size
∆ίνουν αντίστοιχα την ταυτότητα της κάθε διεργασίας και το πλήθος των διεργασιών.
Μια παράµετρος που πρέπει να περάσουµε σ’ αυτές τις συναρτήσεις είναι ο λεγόµενος
‘communicator’. Ο communicator είναι ένα αφηρηµένο αντικείµενο το οποίο
χρησιµοποιείται για να διαµερίζει τις διεργασίες όσον αφορά στις επικοινωνιακές τους
δυνατότητες. Ο communicator ο οποίος συχνά είναι ο µόνος που χρειάζονται οι
περισσότερες εφαρµογές είναι ο προκαθορισµένος MPI-COMM-WORLD. Όµως ο
χρήστης µπορεί να δηµιουργήσει και άλλους communicators. Ο πλήρης κώδικας του
προγράµµατος είναι ο ακόλουθος:
#include <mpi.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
int i;
int id;
int p;
void check_circuit(int,int);
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
for (i=id;i<65536;i+=p)
check_circuit(id,i);
}
printf(“Process %d is done\n”,id);
fflush(stdout);
MPI_Finalize();
return 0;
#define EXTRACT_BIT(n,i) ((n&(1<<i))?1:0)
void check_circuit(int id,int z) {
int v[16];
int i;
57
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
for (i=0;i<16;I++) v[i]=EXTRACT_BIT(z,i);
if ((v[0] || v[1]) && (!v[1] ||!v[3]) && (v[2] || v[3])
&& (!v[3] || !v[4]) && (v[4] || !v[5])
&& (v[5] || !v[6]) && (v[5] || v[6])
&& (v[6] || v[15]) && (v[7] || !v[8])
&& (!v[7] || !v[13]) && (v[8] || v[9])
&& (v[8] || !v[9]) && (!v[9] || !v[10])
&& (v[9] || v[11]) && (v[10] || v[11])
&& (v[12] || v[13]) && (v[13] || !v[14])
&& (v[14] || v[15])) {
printf(“%d) %d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d\n”, id
v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9],
v[10],v[11],v[12],v[13],v[14],v[15]);
fflush(stdout);
}
}
Η κάθε διεργασία ελέγχει 65536 / p συνδυασµούς που επιλέγονται µε την τεχνική της
διαφύλλωσης. Αν µια διεργασία ανακαλύψει έναν αληθή συνδυασµό τότε το αναφέρει
(εκτυπώνει το συνδυασµό).
Η συνάρτηση MPI-Finalize
Η συνάρτηση αυτή καλείται για να τερµατιστεί η λειτουργία του MPI και να επιστραφεί
στο σύστηµα η µνήµη που δεσµεύεται.
Μεταφράζοντας ένα πρόγραµµα MPI
Η τυπική εντολή που γράφουµε στο σύστηµα είναι:
% mpicc -0 sat1 sat1.c
κατά την οποία το πρόγραµµα sat1.c µεταφράζεται και ο εκτελέσιµος κώδικας
αποθηκεύεται στο αρχείο sat1
Τρέχοντας ένα πρόγραµµα MPI
Η τυπική εντολή είναι:
% mpirun –npx sat1
όπου το όρισµα x που ακολουθεί το np είναι ο αριθµός των διεργασιών που θα
δηµιουργηθούν. Ας δούµε µερικές εξόδους για διάφορες τιµές του x
% mpirun –np 1 sat1
0) 101 011 111 001 1001
0) 011 011 111 001 1001
0) 111 011 111 001 1001
0) 101 011 111 101 1001
0) 011 011 111 101 1001
0) 111 011 111 101 1001
0) 101 011 111 011 1001
0) 011 011 111 011 1001
0) 111 011 111 011 1001
Process 0 is done
58
∆.Σ. Βλάχος και Θ.Η. Σίµος
% mpirun –np 2 sat1
0) 101 011 111 001 1001
0) 011 011 111 001 1001
0) 111 011 111 001 1001
1) 101 011 111 101 1001
1) 011 011 111 101 1001
1) 111 011 111 101 1001
1) 101 011 111 011 1001
1) 011 011 111 011 1001
1) 111 011 111 011 1001
Process 0 is done
Process 1 is done
% mpirun –np 3 sat1
0) 101 011 111 001 1001
0) 011 011 111 001 1001
2) 111 011 111 001 1001
1) 101 011 111 101 1001
1) 011 011 111 101 1001
1) 111 011 111 101 1001
0) 101 011 111 011 1001
2) 011 011 111 011 1001
2) 111 011 111 011 1001
Process 1 is done
Process 2 is done
Process 0 is done
Βλέπουµε ότι σε όλες τις περιπτώσεις το ίδιο σύνολο αληθών συνδυασµών
ανακαλύπτεται αλλά κάθε φορά διαφορετική εργασία µπορεί να τον ανακαλύψει.
Σηµειώστε εδώ ότι η ακολουθία µε την οποία εκτυπώνονται τα µηνύµατα τέλους κάθε
διεργασίας δε σηµαίνει ότι είναι και η ακολουθία µε την οποία τερµατίζονται οι
διεργασίες. Αυτή είναι µια παγίδα που οι προγραµµατιστές των παράλληλων
προγραµµάτων πρέπει να αποφεύγουν. Ποτέ µη βασιστείτε στις εντολές εξόδου για να
διαπιστώσετε θέµατα που έχουν να κάνουν µε το χρονισµό των διεργασιών!
Καθολικές επικοινωνίες
Με τον όρο καθολική επικοινωνία εννοούµε µια διαδικασία επικοινωνίας κατά την οποία
ένα σύνολο από διεργασίες ανταλλάσσουν µια οµάδα δεδοµένων. Ας υποθέσουµε ότι στο
προηγούµενο παράδειγµα θέλουµε να εκτυπώνεται και ο αριθµός των συνδυασµών που
ικανοποιούν το κύκλωµα.
Για να το πετύχουµε αυτό, αλλάζουµε τη συνάρτηση check_circuit ώστε να επιστρέφει
την ακέραια τιµή 1 αν ο συνδυασµός ικανοποιεί το κύκλωµα αλλιώς 0.Ο τροποποιηµένος
κώδικας είναι:
#include <mpi.h>
#include <stdio.h>
59
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
int main(int argc, char* argv[]) {
int global_solutions;
int i;
int id;
int p;
int solutions;
int check_circuit(int,int);
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
solutions=0;
for (i=id;i<65536;i+=p)
solutions+=check_circuit(id,i);
MPI_Reduce(&solutions,&global_solutions,1,MPI_INT,
MPI_SUM,0,MPI_COMM_WORLD);
printf(“Process %d is done\n”,id);
fflush(stdout);
MPI_Finalize();
}
if (id==0)
printf(“There are %d different solutions\n”,
global_solutions);
return 0;
#define EXTRACT_BIT(n,i) ((n&(1<<i))?1:0)
int check_circuit(int id,int z)
int v[16];
int i;
{
for (i=0;i<16;I++) v[i]=EXTRACT_BIT(z,i);
if ((v[0] || v[1]) && (!v[1] ||!v[3]) && (v[2] || v[3])
&& (!v[3] || !v[4]) && (v[4] || !v[5])
&& (v[5] || !v[6]) && (v[5] || v[6])
&& (v[6] || v[15]) && (v[7] || !v[8])
&& (!v[7] || !v[13]) && (v[8] || v[9])
&& (v[8] || !v[9]) && (!v[9] || !v[10])
&& (v[9] || v[11]) && (v[10] || v[11])
&& (v[12] || v[13]) && (v[13] || !v[14])
&& (v[14] || v[15])) {
printf(“%d) %d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d\n”, id
v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9],
v[10],v[11],v[12],v[13],v[14],v[15]);
fflush(stdout);
60
∆.Σ. Βλάχος και Θ.Η. Σίµος
return 1;
}
return 0;
}
Εδώ υπάρχουν δυο επιπλέον µεταβλητές , solutions και global-solutions. Στη µεταβλητή
solutions αθροίζονται οι συνδυασµοί που ικανοποιούν το κύκλωµα και έχουν ελεγχθεί
από τη συγκεκριµένη διεργασία. Η µεταβλητή global-solutions όπως θα δούµε έχει νόηµα
µόνο για τη διεργασία 0.
Η συνάρτηση MPI-Reduce
Η συνάρτηση MPI-Reduce εκτελεί µια πράξη αναγωγής σε δεδοµένα από όλους τους
υπολογιστές. Η σύνταξή της είναι :
Int MPI-Reduce
(
Void *operand,
/* διεύθυνση του πρώτου ορίσµατος
αναγωγής*/
Void
*result,
/* διεύθυνση του πρώτου
αποτελέσµατος*/
Int
count,
/* πλήθος αναγωγών*/
MPI-Database type, /* τύπος των δεδοµένων*/
MPI-Op operator,
/* είδος αναγωγής*/
Int root,
/* η διεργασία που θα λάβει το
αποτέλεσµα*/
MPI-Comm comn)
/*communicator*/
Ας δούµε λίγο τα ορίσµατα της συνάρτησης ένα προς ένα: Το πρώτο όρισµα είναι ένας
δείκτης σε µια περιοχή της µνήµης που αποθηκεύονται τα δεδοµένα που πρόκειται να
αναχθούν. Το δεύτερο όρισµα είναι ένας δείκτης σε µια περιοχή της µνήµης όπου θα
αποθηκευτούν τα αποτελέσµατα. Το όρισµα αυτό έχει νόηµα µόνο για τη διεργασία που
θα λάβει το αποτέλεσµα. Το τρίτο όρισµα περιλαµβάνει τον αριθµό των αναγωγών που
θα εκτελεστούν. Σηµειώστε εδώ ότι αν ο αριθµός των αναγωγών είναι µεγαλύτερος του 1,
τότε και τα δεδοµένα εισόδου και το αποτέλεσµα πρέπει να είναι µονοδιάστατοι πίνακες.
Το τέταρτο και πέµπτο όρισµα δείχνουν τον τύπο των δεδοµένων και το είδος της
αναγωγής. Οι επιλογές που έχουµε εδώ φαίνονται στους πίνακες 1,2
Πίνακας 1. Οι σταθερές MPI και οι αντίστοιχες της C.
Όνοµα
MPI_CHAR
MPI_DOUBLE
MPI_FLOAT
MPI_INT
MPI_LONG
MPI_LONG_DOUBLE
MPI_SHORT
MPI_UNSIGNED_CHAR
Τύπος στη C
signed char
Double
Float
Int
Long
long double
Short
unsigned char
61
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_UNSIGNED
MPI_UNSIGNED_LONG
MPI_UNSIGNED_SHORT
unsigned int
unsigned long
unsigned short
Πίνακας 2. Αναγωγές που υποστηρίζει το MPI.
Όνοµα
MPI_BAND
MPI_BOR
MPI_BXOR
MPI_LAND
MPI_LOR
MPI_LXOR
MPI_MAX
MPI_MAXLOC
MPI_MIN
MPI_MINLOC
MPI_PROD
MPI_SUM
Σηµασία
Bitwise and
Bitwise or
Bitwise exclusive or
Logical and
Logical or
Logical exclusive or
Maximum
Maximum and location of maximum
Minimum
Minimum and location of minimum
Product
Sum
Τέλος το έκτο όρισµα δείχνει την ταυτότητα της διεργασίας που θα λάβει το αποτέλεσµα
και το τελευταίο όρισµα του communicator.
Ένα σηµείο που πρέπει να προσέχουµε όταν χρησιµοποιούµε την MPI-Reduce όπως και
άλλες συναρτήσεις που υλοποιούν καθολικές επικοινωνίες, είναι ότι όλες οι διεργασίες
πρέπει να καλέσουν τη συνάρτηση. Αν όλες οι διεργασίες δεν καλέσουν τη συνάρτηση
τότε το πρόγραµµα θα κρεµάσει.
Ένα άλλο σηµείο που παρατηρούµε στη συνάρτηση main είναι η επιλεκτική χρήση της
µεταβλητής global-solutions. ∆είτε ότι µόνο η διεργασία 0 εκτυπώνει την τιµή της αφού
µόνο αυτή έχει το σωστό αποτέλεσµα. Τέλος, η έξοδος τώρα του προγράµµατος θα είναι:
% mpirun –np 3 sat1
0) 101 011 111 001 1001
0) 011 011 111 001 1001
2) 111 011 111 001 1001
1) 101 011 111 101 1001
1) 011 011 111 101 1001
1) 111 011 111 101 1001
0) 101 011 111 011 1001
2) 011 011 111 011 1001
2) 111 011 111 011 1001
Process 1 is done
Process 2 is done
Process 0 is done
There are 9 different solutions
62
∆.Σ. Βλάχος και Θ.Η. Σίµος
Μετρώντας την απόδοση
Μπορούµε να µετρήσουµε την απόδοση του προγράµµατος µας χρησιµοποιώντας τις
συναρτήσεις MPI_Wtime και MPI_Wtick. Η µεν πρώτη επιστρέφει το χρόνο σε
δευτερόλεπτα από κάποιο σηµείο ( Το σηµείο αυτό είναι αυθαίρετο, οπότε η Wtime
χρησιµοποιείται διαφορικά), ενώ η δεύτερη την ακρίβεια του αποτελέσµατος.
Επειδή σε ένα παράλληλο σύστηµα δεν µπορούµε να υποθέσουµε ότι όλες οι διεργασίες
ξεκινούν µαζί ή ότι περνούν από ένα σηµείο του προγράµµατος ταυτόχρονα µπορούµε
να χρησιµοποιήσουµε τη συνάρτηση MPI_Barrier. Η συνάρτηση αυτή παίρνει σαν
όρισµα έναν communicator και φράζει τη λειτουργία όλων των διεργασιών µέχρι όλες να
φτάσουν σ’ αυτό το σηµείο του κώδικα. Έτσι µπορούµε να µετρήσουµε το χρόνο του
προγράµµατός µας ως εξής:
MPI_Ιnit(Ravgc, Rangv);
MPI_Barrier(MPI_COMM_WORLD);
Elapsed_time = -MPI_Wtime( );
Στη συνέχεια µετά την εκτέλεση της εντολής
προσθέσουµε:
MPI_Reduce
µπορούµε να
Elapsed_time +=MPI_Wtime( );
Στο παρακάτω σχήµα φαίνεται η απόδοση του προγράµµατός µας σαν συνάρτηση του
αριθµού των επεξεργαστών
Σχήµα 4. Μέσος χρόνος εκτέλεσης σαν συνάρτηση του αριθµού των επεξεργαστών. Η
διακεκοµµένη γραµµή παριστάνει την ιδανική καµπύλη. Η απόκλιση οφείλεται στον
επικοινωνιακό χρόνο και µεγαλώνει µε τον αριθµό των επεξεργαστών.
63
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Παρατηρείστε ότι η απόκλιση από την ιδανική καµπύλη αυξάνει µε την αύξηση των
επεξεργαστών πράγµα που οφείλεται στον επικοινωνιακό χρόνο που προστίθεται στον
υπολογιστικό χρόνο του αλγόριθµού µας.
64
∆.Σ. Βλάχος και Θ.Η. Σίµος
Το κόσκινο του
Ερατοσθένη
Εισαγωγή
Το κόσκινο του Ερατοσθένη είναι µια χρήσιµη εφαρµογή µέσα από την οποία µπορούµε
να δούµε µερικά ενδιαφέροντα θέµατα που προκύπτουν όταν προγραµµατίζουµε µε
χρήση του MPI. Η µέθοδος που θα ακολουθήσουµε εδώ εστιάζεται στη διάσπαση του
πεδίου ορισµού του προβλήµατος (παραλληλισµός δεδοµένων). Κατά τη σχεδίαση της
συσσώρευσης θα ελέγξουµε την απόδοση διάφορων επιλογών που έχουµε. Στη συνέχεια
θα εξετάσουµε τη δυνατότητα βελτίωσης του αλγόριθµου που θα αναπτύξουµε. Στα
πλαίσια αυτού του κεφαλαίου θα εισάγουµε τη µέθοδο MPI_Bcast µε την οποία
µπορούµε να στείλουµε δεδοµένα σε όλες τις διεργασίες. Επιπλέον, θα αναπτύξουµε µια
γενική µέθοδο για τεµαχισµό κατά συνεχόµενα τµήµατα ενός συνόλου δεδοµένων.
Σειριακός αλγόριθµος
Ο σκοπός µας είναι να αναπτύξουµε έναν παράλληλο αλγόριθµο που να βασίζεται στη
µέθοδο που ανακάλυψε ο Ερατοσθένης (276-194 π.Χ.) για την εύρεση των πρώτων
αριθµών που είναι µικρότεροι ή ίσοι µε ένα δοσµένο αριθµό n. Ο ψευτο-κώδικας του
αλγόριθµου του Ερατοσθένη έχει ως εξής:
1.
∆ηµιουργούµε ένα διάνυσµα µε τους αριθµούς από το 2 ως το n.
2.
Έστω k=2.
3.
Σηµειώνουµε όλα τα πολλαπλάσια του k µεταξύ του k2 και του n.
4.
Βρίσκουµε το µικρότερο ακέραιο που είναι µεγαλύτερος από τον k και
δεν έχει σηµειωθεί.
5.
Αν για το νέο k ισχύει k2<n πηγαίνουµε πάλι στο βήµα 3.
6.
Οι ακέραιοι που δεν έχουν σηµειωθεί είναι οι πρώτοι αριθµοί που είναι
µικρότεροι ή ίσοι από το n.
Η εφαρµογή του αλγόριθµου του Ερατοσθένη για n=60 φαίνεται στο σχήµα 1.
65
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 1. Σχηµατική παράσταση του αλγόριθµου του Ερατοσθένη για τον προσδιορισµό των
πρώτων αριθµών που είναι µικρότεροι ή ίσοι από ένα δοσµένο αριθµό. Στο (a)
σηµειώνονται τα πολλαπλάσια του 2. Στο (b) σηµειώνονται τα πολλαπλάσια του 3 από το 9
έως το 60. Στο (c) σηµειώνονται τα πολλαπλάσια του 5 από το 25 έως το 60. Στο (d)
σηµειώνονται τα πολλαπλάσια του 7 από το 49 έως το 60. Ο επόµενος πρώτος είναι το 11,
αλλά επειδή το 112 είναι µεγαλύτερο του 60, ο αλγόριθµος σταµατά.
Όσον αφορά την αναπαράσταση των αριθµών σε ένα πρόγραµµα C, µπορούµε να
χρησιµοποιήσουµε έναν πίνακα n-1 χαρακτήρων που θα αντιστοιχούν στους ακέραιους
από το 2 ως το n. Έτσι, η τιµή 1 στη θέση –i του πίνακα θα αντιστοιχεί στο αν ο ακέραιος
i+2 έχει σηµειωθεί.
Πηγές παραλληλισµού
Ας δούµε λίγο πως µπορούµε να διαµερίσουµε τον αλγόριθµο που αναπτύξαµε. Επειδή ο
στόχος µας είναι να σηµειώσουµε n-1 στοιχεία, µια φυσιολογική διάσπαση είναι να
αντιστοιχίσουµε σε κάθε αρχέγονη διαδικασία έναν από τους n-1 αριθµούς. Έτσι, η κάθε
αρχέγονη διεργασία θα είναι υπεύθυνη να αποφασίσει για το αν ο αριθµός στον οποίο
έχει αντιστοιχηθεί θα σηµειωθεί ή όχι, ελέγχοντας το υπόλοιπο της διαίρεσής του µε
κάποιο k.
66
∆.Σ. Βλάχος και Θ.Η. Σίµος
Στη συνέχεια χρειάζεται να γίνει µια αναγωγή για τον προσδιορισµό της νέας τιµής του k,
και έπειτα η νέα αυτή τιµή πρέπει να µεταδοθεί σε όλες τις διεργασίες (broadcast).
Το κλειδί λοιπόν είναι να δούµε πως µπορούµε να επιτύχουµε µια τέτοια συσσώρευση
που να µειώνει τις αναγωγές και τις µεταδόσεις σε όλες τις διεργασίες.
∆ιάσπαση δεδοµένων
Μετά τη συσσώρευση, µια διεργασία θα είναι υπεύθυνη για ένα σύνολο από αριθµούς
που πρέπει να ελεγχθούν. Ας δούµε µερικές επιλογές για το πως µπορεί να γίνει κάτι
τέτοιο:
1.
∆ιάσπαση µε διαφύλλωση
Στην περίπτωση αυτή η αντιστοιχία µπορεί να γίνει ως εξής:
•
Η διεργασία 0 είναι υπεύθυνη για τους αριθµούς 2,2+p,2+2p,…
•
Η διεργασία 1 είναι υπεύθυνη για τους αριθµούς 3,3+p,3+2p,…
κ.ο.κ.
Έτσι, το στοιχείο –i ελέγχεται από τη διεργασία (i-2) mod p. Το µειονέκτηµα εδώ είναι
ότι µπορεί για το συγκεκριµένο πρόβληµα να προκύψει ανοµοιοµορφία στην κατανοµή
του υπολογιστικού φόρτου ανάµεσα στις διεργασίες και επιπλέον δε φαίνεται αυτού του
είδους η διάσπαση να µειώνει τις αναγωγές και τις επικοινωνίες.
2.
∆ιάσπαση µε οµάδες
Μια εναλλακτική προσέγγιση είναι να οµαδοποιήσουµε συνεχόµενους αριθµούς σε µια
διεργασία. Ας υποθέσουµε ότι έχουµε n-αριθµούς και p-διεργασίες. Αν n mod p =0 τότε
αντιστοιχίζουµε n/p συνεχόµενους αριθµούς σε κάθε διεργασία. Έστω τώρα η γενική
περίπτωση όπου n mod p = r. Θα συµβολίσουµε µε n/p το µικρότερο ακέραιο d για τον
οποίο ισχύει d≥n/p και µε n/p το µεγαλύτερο ακέραιο d για τον οποίο ισχύει d≤n/p.
Μπορούµε τώρα να αντιστοιχίσουµε στις –r πρώτες διεργασίες n/p αριθµούς και στις
υπόλοιπες p-r διεργασίες n/p αριθµούς.
Για παράδειγµα ας υποθέσουµε ότι έχουµε 1024 αριθµούς και 10 διεργασίες. Επειδή
1024 mod 10 = 4, µπορούµε να αντιστοιχίσουµε 1024/10=103 αριθµούς στις 4 πρώτες
διεργασίες και στις υπόλοιπες 10-4=6 διεργασίες να αντιστοιχίσουµε 1024/10=102
αριθµούς (ισχύει ότι 4*103+6*102=1024).
Ας δούµε λίγο τώρα ποια στοιχεία ελέγχει η –i διεργασία.
67
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
i<r
Τότε το πρώτο στοιχείο της διεργασίας θα είναι το:
i ⋅  n / p  = i ⋅ (  n / p  + 1)
= i ⋅  n / p  + i
i≥r
Τότε το πρώτο στοιχείο της διεργασίας θα είναι το:
r ⋅  n / p  + (i − r )  n / p  = r ⋅  n / p  + r + (i − r )  n / p 
= i ⋅  n / p  + r
Συνδυάζοντας τα παραπάνω αποτελέσµατα, το πρώτο στοιχείο της –i διεργασίας σε κάθε
περίπτωση θα είναι το
i ⋅  n / p  + min{i, r}
Το τελευταίο στοιχείο της –i διεργασίας θα είναι το προηγούµενο από το πρώτο στοιχείο
της –(i+1) διεργασίας, δηλαδή το
(i + 1) ⋅  n / p  + min{i + 1, r} − 1
Μπορεί τέλος να δειχθεί ότι το στοιχείο –j θα ανήκει στη διεργασία:

 ( j − r )

min   j
,


 n / p   
  (  n / p  + 1)  
Σηµειώστε εδώ ότι η πιο πάνω τεχνική αντιστοιχεί στις –r πρώτες διεργασίες τους
περισσότερους αριθµούς.
Μια άλλη οµαδοποίηση, την οποία θα υιοθετήσουµε, µπορεί να επιτευχθεί ως εξής: Το
πρώτο στοιχείο της –i διεργασίας είναι το
i ⋅ n / p 
και το τελευταίο είναι το
 (i + 1) ⋅ n / p  − 1
Τότε, το –j στοιχείο θα ανήκει στη διεργασία
 ( p ( j + 1) − 1) / n 
Στο σχήµα 2 φαίνονται οι δύο οµαδοποιήσεις για την περίπτωση που έχουµε 14 στοιχεία
και 4 διεργασίες.
68
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 2. Οµαδοποίηση 14 στοιχείων σε 4 διεργασίες.
Παρακάτω δίνουµε µερικές µακροεντολές µε τις οποίες θα κάνουµε το πρώτο και το
τελευταίο στοιχείο µιας διεργασίας, πόσα στοιχεία έχει µια διεργασία και σε ποια
διεργασία ανήκει ένα στοιχείο:
#define MIN(a,b) ((a)<(b)?(a):(b))
#define MAX(a,b) ((a)>(b)?(a):(b))
//
#define BLOCK_LOW(id,p,n) ((id)*(n)/(p))
#define BLOCK_HIGH(id,p,n) (BLOCK_LOW((id)+1,p,n)-1)
#define BLOCK_SIZE(id,p,n) (BLOCK_HIGH(id,p,n)BLOCK_LOW(id,p,n)+1)
#define BLOCK_OWNER(j,p,n) (((p)*((j)+1)-1)/(n))
Αξίζει να σηµειώσουµε εδώ πως προσοχή πρέπει να δείξουµε στην οµαδοποίηση όταν
αναφερόµαστε στα στοιχεία του διανύσµατος που έχουµε διαµερίσει. Ο καθολικός
δείκτης του διανύσµατος είναι διαφορετικός από τον τοπικό δείκτη στο τµήµα του
διανύσµατος που κατέχει η κάθε διεργασία. Έτσι είναι απαραίτητο να µετασχηµατίσουµε
το δείκτη αυτόν πριν τον χρησιµοποιήσουµε. Στο σχήµα 3 φαίνεται ένα τέτοιο
παράδειγµα.
Σχήµα 3. Αντιστοιχία γενικού και τοπικού δείκτη σε έναν οµαδοποιηµένο πίνακα.
Τέλος, δεδοµένου ότι ο µεγαλύτερος ακέραιος που θα ελεγχθούν τα πολλαπλάσια είναι ο
√n, αν οι ακέραιοι που θα αντιστοιχηθούν στην πρώτη διεργασία είναι από 2 ως √n, τότε
θα εξοικονοµήσουµε ένα µήνυµα.
69
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ανάπτυξη του παράλληλου αλγόριθµου
Τώρα που έχουµε ολοκληρώσει τον τεµαχισµό των δεδοµένων µπορούµε να
επιστρέψουµε στο σειριακό αλγόριθµο. Το πρώτο βήµα είναι εύκολο να το υλοποιήσουµε
µε τη διαφορά τώρα ότι κάθε διεργασία είναι υπεύθυνη για την αρχικοποίηση του υποπίνακα των αριθµών που κατέχει. Θα θεωρήσουµε εδώ ότι η πρώτη διεργασία περιέχει
όλους τους αριθµούς µέχρι το √n.
Κάθε διεργασία θα πρέπει να γνωρίζει την τιµή του k προκειµένου να σηµειώσει τα
πολλαπλάσια του k µέσα στο σύνολο των αριθµών που διαθέτει. Για το λόγο αυτό, κάθε
διεργασία θα εκτελέσει το βήµα 2 του σειριακού αλγόριθµου.
Το βήµα 3 του σειριακού αλγόριθµου είναι εύκολο να µεταφραστεί. Κάθε διεργασία θα
βρει και θα σηµειώσει τα πολλαπλάσια του k µέσα στον υπο-πίνακα που διαθέτει.
Επειδή η διαδικασία 0 είναι αυτή που θα υπολογίσει την επόµενη τιµή του k, θα πρέπει
να τη µεταδώσει σε όλες τις άλλες διεργασίες. Η λειτουργία αυτή θα επιτευχθεί µε τη
χρήση της συνάρτησης MPI_Bcast.
Η συνάρτηση MPI_Bcast
int MPI_Bcast
void
int
MPI_Datatype
int
MPI_Comm
(
*buffer,
count,
datatype,
root,
comm)
Όπου:
buffer είναι ένας δείκτης σε µια περιοχή µνήµης που είναι αποθηκευµένα τα δεδοµένα
που πρόκειται να αποσταλούν (για τη διεργασία που στέλνει) και ένας δείκτης σε µια
περιοχή µνήµης που θα αποθηκευτούν τα δεδοµένα που θα ληφθούν ( για τις υπόλοιπες
διεργασίες)
count είναι ο αριθµός των στοιχείων
datatype είναι ο τύπος των στοιχείων
root είναι ο αριθµός της διεργασίας που στέλνει τα δεδοµένα και
comm είναι ο communicator στις διεργασίες του οποίου θα υλοποιηθεί η µετάδοση.
Στο παρακάτω σχήµα φαίνεται ο γράφος εργσία/κανάλι για τον αλγόριθµο του
Ερατοσθένη.
70
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 4. Γράφος κανάλι/εργασία για την παράλληλη εκδοχή του αλγόριθµου του
Ερατοσθένη.
Ανάλυση του παράλληλου αλγόριθµου
Έστω χ είναι ο χρόνος που απαιτείται για να σηµειωθεί ένας αριθµός. Στο χρόνο αυτό
περιλαµβάνεται και ο χρόνος για την αύξηση του δείκτη και τον έλεγχο του τερµατισµού.
Ο σειριακός αλγόριθµος έχει πολυπλοκότητα Θ(n ln ln n). Χονδρικά µπορούµε να πούµε
πως ο χρόνος που απαιτείται για το σειριακό αλγόριθµο είναι χ n ln ln n.
Αφού µόνο ένα στοιχείο µεταδίδεται σε κάθε επανάληψη, ο χρόνος που απαιτείται για
κάθε µετάδοση είναι λlog p, όπου λ είναι η καθυστέρηση που εισάγει η αποστολή ενός
µηνύµατος.
Υπάρχουν περίπου n/ln n πρώτοι αριθµοί µικρότεροι του n. Άρα, µια καλή προσέγγιση
για τον αριθµό των αποστολών είναι √n / ln √n.
Ο συνολικός χρόνος του παράλληλου αλγόριθµου θα είναι τότε:
χ (n ln ln n) / p + ( n / ln n )λ log p 
Ο πλήρης κώδικας
Παρακάτω δίνουµε τον πλήρη κώδικα για την υλοποίηση του αλγόριθµου του
Ερατοσθένη σε έναν παράλληλο υπολογιστή.
#include
#include
#include
#include
<mpi.h>
<math.h>
<stdio.h>
<stdlib.h>
71
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
#include "mympi.h"
int main (int argc, char* argv[])
{
int
count;
/*local prime count*/
double
elapsed_time; /*parallel execution time*/
int
first;
/*index of first multiple*/
int
global_count; /*global prime count*/
int
high_value;
/*highest value on this proc*/
int
i;
int
id;
/*process id number*/
int
index;
/*index of current prime*/
int
low_value;
/*lowest value on this proc*/
char* marked;
/*portion of 2,...'n'*/
int
n;
/*sievimg from 2,...'n'*/
int
p;
/*number of processes*/
int
proc0_size;
/*size of proc 0's subarray*/
int
prime;
/*current prime*/
int
size;
/*elements in 'marked'*/
MPI_Init(&argc,&argv);
/* Start the timer*/
MPI_Barrier(MPI_COMM_WORLD);
elapsed_time=-MPI_Wtime();
/* Get id and p*/
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
/*Check for command line argument*/
if (argc!=2)
{
if (!id)
printf ("Usage: %s <m>\n",argv[0]);
MPI_Finalize();
exit(1);
}
n=atoi(argv[1]);
/*Get low and high values*/
low_value=2+BLOCK_LOW(id,p,n-1);
high_value=2+BLOCK_HIGH(id,p,n-1);
size=BLOCK_SIZE(id,p,n-1);
/*Check if all primes used for sieving are contained in
proc 0*/
proc0_size=(n-1)/p;
if ((2+proc0_size)<(int)sqrt((double)n)) {
if (!id)
printf("Too many processes\n");
MPI_Finalize();
exit(1);
72
∆.Σ. Βλάχος και Θ.Η. Σίµος
}
/*Ok. Allocate processe's space*/
marked=(char*)malloc(size);
if (marked==NULL) {
printf("Cannot allocate enough memory\n");
MPI_Finalize();
exit(1);
}
/*Initialize*/
for (i=0;i<size;i++)
marked[i]=0;
if (!id)
index=0;
prime=2;
/*Start sieving*/
do {
if (prime*prime>low_value)
first=prime*prime-low_value;
else {
if (!(low_value % prime))
first=0;
else
first=prime-(low_value % prime);
}
for (i=first;i<size;i+=prime)
marked[i]=1;
if (!id) {
while (marked[++index]);
prime=index+2;
}
MPI_Bcast(&prime,1,MPI_INT,0,MPI_COMM_WORLD);
} while(prime*prime<=n);
/*Collect results*/
count=0;
for (i=0;i<size;i++) {
if (!marked[i])
count++;
}
MPI_Reduce(&count,&global_count,1,MPI_INT,MPI_SUM,0,MPI_C
OMM_WORLD);
/*Stop the timer*/
elapsed_time+=MPI_Wtime();
/*Print the results*/
73
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
if (!id) {
printf("%d primes are less than or equal to
%d.\n",global_count,n);
printf("Total elapsed time: %10.6f.\n",elapsed_time);
}
MPI_Finalize();
return 0;
}
Απόδοση
Στο σχήµα 6 φαίνεται η εκτιµώµενη και πραγµατική καµπύλη απόδοσης για τον
αλγόριθµο που αναπτύξαµε.
Σχήµα 6. Θεωρητική και πειραµατική καµπύλη απόδοσης για την παράλληλη εκδοχή του
αλγόριθµου του Ερατοσθένη.
74
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ο αλγόριθµος του Floyd
Εισαγωγή
Πολύ συχνά οι ταξιδιωτικοί χάρτες περιέχουν πίνακες στους οποίους αναγράφονται οι
χιλιοµετρικές αποστάσεις µεταξύ των διαφόρων πόλεων. Οι γραµµές και οι στήλες ενός
τέτοιου πίνακα αντιστοιχούν στις διάφορες πόλεις και το στοιχείο του πίνακα που
αντιστοιχεί σε δύο πόλεις (µία στη γραµµή και µία στη στήλη) είναι η χιλιοµετρική
απόσταση µεταξύ τους. Ο αλγόριθµος του Floyd είναι µια κλασσική µέθοδος που παράγει
τέτοιους πίνακες.
Στο κεφάλαιο αυτό θα εξετάσουµε µια παράλληλη εκδοχή του αλγόριθµου του Floyd.
Επιπλέον, θα εισάγουµε δύο νέες συναρτήσεις MPI που υλοποιούν επικοινωνία µεταξύ
διεργασιών, την MPI_Send και MPI_Recv.
Εύρεση κοντινότερων διαδροµών
Ένας προσανατολισµένος γράφος µε βάρη, είναι ένα σύνολο V από κόµβους και ένα
σύνολο E από ακµές, οι οποίες έχουν αρχή και τέλος και σε κάθε ακµή έχουµε
αντιστοιχίσει έναν αριθµό (βάρος). Ένα παράδειγµα ενός τέτοιου γράφου φαίνεται στο
σχήµα 1.
Σχήµα 1. Ένας προσανατολισµένος γράφος µε βάρη.
Σε κάθε τέτοιο γράφο µπορούµε να αντιστοιχίσουµε έναν πίνακα γειτνίασης, στον οποίο
οι γραµµές και οι στήλες αντιστοιχούν στους κόµβους του γράφου και το στοιχείο –ij του
πίνακα είναι το βάρος της ακµής που έχει αρχή το –i και τέλος το –j (αν υπάρχει) ή
άπειρο σε αντίθετη περίπτωση. Επιπλέον, µπορούµε να κατασκευάσουµε και τον πίνακα
κοντινότερων διαδροµών στον οποίο και πάλι οι γραµµές και οι στήλες αντιστοιχούν
στους κόµβους του γράφου και το στοιχείο –ij αντιστοιχεί στην κοντινότερη διαδροµή
75
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
µεταξύ των δύο κόµβων. Στο σχήµα 2 φαίνονται ο πίνακας γειτνίασης και ο πίνακας
κοντινότερων διαδροµών για το γράφο του σχήµατος 1.
Σχήµα 2. Πίνακας γειτνίασης και πίνακας κοντινότερων διαδροµών για το γράφο του
σχήµατος 1.
Με δεδοµένο ένα πίνακα γειτνίασης, ο αλγόριθµος του Floyd κατασκευάζει τον πίνακα
κοντινότερων διαδροµών. Η βασική ιδέα του αλγόριθµου είναι να συγκρίνει µε
εξαντλητικό τρόπο την απόσταση µεταξύ δύο κόµβων µε την απόσταση που προκύπτει
αν κινηθούµε από τον έναν κόµβο στον άλλο µέσω κάποιου τρίτου. Προφανώς, µια
τέτοια εξαντλητική σύγκριση βασίζεται στην παρατήρηση ότι άν γνωρίζουµε τις
ελάχιστες διαδροµές µεταξύ του κόµβου –i και όλων των κόµβων –k και τις ελάχιστες
διαδροµές µεταξύ των κόµβων –k και του κόµβου –j, τότε :
d min (i, j ) = min{d min (i, k ) + d min (k , j )}
k
Ο ψευτο-κώδικας του αλγόριθµου του Floyd δίνεται παρακάτω:
Είσοδος: n- ο αριθµός των κόµβων
a[0..n-1,0..n-1] ο πίνακας γειτνίασης
Έξοδος: Ο πίνακας a µετασχηµατισµένος σε πίνακα
κοντινότερων διαδροµών
for k=0 to n-1
for i=0 to n-1
for j=0 to n-1
a[i,j]=min(a[i,j],a[i,k]+a[k,j])
end for
end for
end for
76
∆.Σ. Βλάχος και Θ.Η. Σίµος
∆υναµική δηµιουργία πινάκων
Στα περισσότερα προγράµµατα που διαχειρίζονται πίνακες, οι διαστάσεις αυτών των
πινάκων αποτελούν τµήµα των δεδοµένων εισόδου. Έτσι, ο προγραµµατιστής, δεν µπορεί
εκ των προτέρων να γνωρίζει αυτές τις διαστάσεις και η δηµιουργία των πινάκων πρέπει
να γίνει στη φάση εκτέλεσης και όχι στη φάση µετάφρασης του προγράµµατος.
Στην περίπτωση µονοδιάστατων πινάκων τα πράγµατα είναι απλά. Αν κάποιος θέλει έναν
πίνακα n-ακεραίων, το µόνο που έχει να κάνει είναι να δεσµεύσει µια συνεχόµενη
περιοχή µνήµης ικανής να φιλοξενήσει τους n-ακέραιους και στη συνέχεια µε έναν δείκτη
να δείχνει στην αρχή αυτής της περιοχής της µνήµης. Έτσι, σε µια τέτοια περίπτωση ο
κώδικας για τη δηµιουργία του πίνακα θα είναι:
int** A;
A=(int *)malloc(n*sizeof(int));
Τότε, το –i στοιχείο του πίνακα θα είναι το:
*(A+i) =A[i]
Στην περίπτωση ενός διδιάστατου πίνακα τα πράγµατα είναι λίγο πιο πολύπλοκα. Έστω
ότι έχουµε να δηµιουργήσουµε έναν πίνακα µε m-γραµµές και n-στήλες. Τότε θα πρέπει
να δεσµεύσουµε µια περιοχή µνήµης ικανή να φιλοξενήσει m*n στοιχεία και στη
συνέχεια να δηµιουργήσουµε m-δείκτες, κάθε ένας από τους οποίους θα δείχνει στην
αρχή της κάθε γραµµής. Ο κώδικας για τη δηµιουργία ενός διδιάστατου πίνακα mγραµµών και n-στηλών θα είναι:
int
**B, *Bstorage, i;
Bstorage=(int*)malloc(m*n*sizeof(int));
B=(int**)malloc(m*sizeof(int*));
for (i=0;i<m;i++)
B[i]=&(Bstorage[i*n]);
Στο σχήµα 3 φαίνεται πως θα είναι δοµηµένη η µνήµη που αντιστοιχίσαµε.
Σχήµα 3. Πίνακας µε διπλή δεικτιοδότηση.
77
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχεδίαση του παράλληλου αλγόριθµου
Τεµαχισµός
Ο φυσιολογικός τεµαχισµός εδώ είναι το κάθε στοιχείο του πίνακα γειτνίασης να
αποτελεί µια αρχέγονη διεργασία. Έτσι, για n-κόµβους θα έχουµε n2 αρχέγονες
διεργασίες.
Επικοινωνία
Κάθε στοιχείο a[i,j] του πίνακα θα επικοινωνήσει µε τα στοιχεία a[k,j] και a[i,k]. Έτσι, η
τιµή ενός στοιχείου που ανήκει στη j-στήλη θα πρέπει να µεταδοθεί σε όλα τα στοιχεία
της j-γραµµής και αντίστροφα τα στοιχεία της j-γραµµής θα πρέπει να µεταδοθούν σε όλα
τα στοιχεία της j-στήλης. Στο σχήµα 4 φαίνονται τα βήµατα για την ενηµέρωση ενός
στοιχείου του πίνακα.
Σχήµα 4. Τεµαχισµός και επικοινωνίες στον αλγόριθµο του Floyd.
78
∆.Σ. Βλάχος και Θ.Η. Σίµος
Συσσώρευση και απεικόνιση
Η παρατήρηση που κάναµε στην προηγούµενη παράγραφο µας οδηγεί στο συµπέρασµα
ότι µια φυσιολογική συσσώρευση θα ήταν κατά γραµµές ή κατά στήλες, αφού κάτι τέτοιο
θα περιόριζε τις επικοινωνίες.
Στη συνέχεια θα θεωρήσουµε ότι η συσσώρευση γίνεται κατά γραµµές, αφού αυτό
διευκολύνει την είσοδο και έξοδο των δεδοµένων. Έτσι, τα στοιχεία µιας διεργασίας θα
πρέπει να µεταδοθούν σε όλες τις άλλες και αυτό απαιτεί για µια γραµµή χρόνο
log p i(λ + n / β )
Τέλος, η απεικόνιση των γραµµών στους επεξεργαστές γίνεται µε την τεχνική που
αναπτύχθηκε στο προηγούµενο κεφάλαιο, θεωρώντας τον πίνακα σαν ένα διάνυσµα
γραµµών.
Σχήµα 5. Τεµαχισµός πίνακα κατά γραµµές και κατά στήλες.
Είσοδος και έξοδος πινάκων
Στο σχήµα 6 φαίνονται τα διάφορα βήµατα για την είσοδο ενός πίνακα που έχει
οµαδοποιηθεί κατά γραµµές.
79
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 6. Σχηµατικό παράδειγµα µιας διαδικασίας εισόδου την οποία διαχειρίζεται µία
µόνο διεργασία.
Στη συνέχεια δίνουµε τις συναρτήσεις για την είσοδο και έξοδο ενός πίνακα. Οι
συναρτήσεις αυτές είναι γενικές και θα τις συµπεριλάβουµε στο αρχείο mympi.h για
µελλοντική χρήση.
80
∆.Σ. Βλάχος και Θ.Η. Σίµος
// Function read_row_striped_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of rows
// to the MPI processes.
void read_row_striped_matrix(
char*
s,
//IN - File name
void***
subs,
//OUT - 2D submatrix indices
void**
storage, //OUT - Submatrix storage area
MPI_Datatype dtype,
//IN - Matrix element type
int*
m,
//OUT - Matrix rows;
int*
n,
//OUT - Matrix cols
MPI_Comm comm)
//IN - Communicator
{
int
datum_size; //Size of matrix elements
int
i;
int
id;
//Process rank
FILE*
infileptr; //Input file pointer
int
local_rows; //Rows on this process
int
p;
//Number of processes
MPI_Status status;
//Result of receive
int
x;
//Result of read
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
datum_size=get_size(dtype);
//Process (p-1) opens file and reads matrix dimensions
if (id==p-1)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*m=0;
else {
fread(m,sizeof(int),1,infileptr);
fread(n,sizeof(int),1,infileptr);
}
}
MPI_Bcast(m,1,MPI_INT,p-1,comm);
//Check for abort
if(!(*m))
MPI_Abort(MPI_COMM_WORLD,OPEN_FILE_ERROR);
//Ok. Broadcast columns
MPI_Bcast(n,1,MPI_INT,p-1,comm);
//
local_rows=BLOCK_SIZE(id,p,*m);
81
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
//Dynamically allocate matrix
*storage=(void*)my_malloc(id,local_rows*(*n)*datum_size);
*subs=(void**)my_malloc(id,local_rows*PTR_SIZE);
for(i=0;i<local_rows;i++)
(*subs)[i]=&(((char*)(*storage))[i*(*n)*datum_size]);
//Process (p-1) reads a block and transmits it
if(id==p-1) {
for(i=0;i<p-1;i++)
{
x=fread(*storage,datum_size,BLOCK_SIZE(i,p,*m)*(*n),infil
eptr);
MPI_Send(*storage,BLOCK_SIZE(i,p,*m)*(*n),dtype,i,DATA_MS
G,comm);
}
x=fread(*storage,datum_size,local_rows*(*n),infileptr);
fclose(infileptr);
}
else
MPI_Recv(*storage,local_rows*(*n),dtype,p1,DATA_MSG,comm,&status);
}
// Function print_row_striped_matrix
// Prints the contents of a matrix that is
// row-distributed among the processes
void print_row_striped_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
comm)
//IN - Communicator
{
MPI_Status
status;
//Result of receive
void
*bstorage; //Elements received from another
process
void
**b;
//2D array indexing 'bstorage'
int
datum_size; //Size of matrix elements
int
i;
int
id;
//Process rank
int
local_rows; //This proc's rows
int
max_block_size;
int
prompt;
//Dummy variable
int
p;
//Number of processes
MPI_Comm_rank(comm,&id);
MPI_Comm_size(comm,&p);
82
∆.Σ. Βλάχος και Θ.Η. Σίµος
local_rows=BLOCK_SIZE(id,p,m);
//Process 0 starts printing
if (!id) {
print_submatrix(a,dtype,local_rows,n);
if (p>1) {
datum_size=get_size(dtype);
max_block_size=BLOCK_SIZE(p-1,p,m);
bstorage=my_malloc(id,max_block_size*n*datum_size);
b=(void**)my_malloc(id,max_block_size*PTR_SIZE);
for (i=0;i<max_block_size;i++)
b[i]=&(((char*)bstorage)[i*n*datum_size]);
//
for (i=1;i<p;i++) {
MPI_Send(&prompt,1,MPI_INT,i,PROMPT_MSG,MPI_COMM_WORLD);
MPI_Recv(bstorage,BLOCK_SIZE(i,p,m)*n,dtype,
i,RESPONSE_MSG,MPI_COMM_WORLD,&status);
print_submatrix(b,dtype,BLOCK_SIZE(i,p,m),n);
}
free(b);
free(bstorage);
}
putchar('\n');
}
else {
MPI_Recv(&prompt,1,MPI_INT,0,PROMPT_MSG,MPI_COMM_WORLD,&s
tatus);
MPI_Send(*a,local_rows*n,dtype,0,RESPONSE_MSG,MPI_COMM_WO
RLD);
}
}
Επικοινωνία σηµείου προς σηµείο
Στο σηµείο αυτό θα εισάγουµε ένα νέο είδος επικοινωνίας µεταξύ των διεργασιών. Μέχρι
εδώ έχουµε δει περιπτώσεις που οι διεργασίες επικοινωνούν για να υλοποιηθεί µια πράξη
αναγωγής ή µια όταν µια διεργασία µεταδίδει τα δεδοµένα της σε όλες τις άλλες
διεργασίες. Η περίπτωση που θα εξετάσουµε τώρα είναι όταν µια διεργασία επικοινωνεί
µια άλλη συγκεκριµένη διεργασία.
Σε µια τέτοια περίπτωση η µια διεργασία στέλνει δεδοµένα σε µια άλλη, ενώ η άλλη
διεργασία έχει διακόψει τη λειτουργία της και περιµένει να λάβει αυτά τα δεδοµένα
(σχήµα 7).
83
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 7. Σχηµατική παράσταση επικοινωνίας σηµείου προς σηµείο.
Οι δύο συναρτήσεις MPI που υλοποιούν αυτήν τη µορφή επικοινωνίας είναι η MPI_Send
και MPI_Recv.
Η συνάρτηση MPI_Send
int MPI_Send (
void*
message,
int
count,
MPI_Datatype
datatype,
int
dest,
int
tag,
MPI_Comm comm.);
Όπου:
message είναι ένας δείκτης σε µια περιοχή µνήµης όπου αποθηκεύονται τα δεδοµένα
που πρόκειται να αποσταλούν
count είναι ο αριθµός των στοιχείων που πρόκειται να αποσταλούν
datatype είναι ο τύπος των δεδοµένων
dest είναι ο αριθµός της διεργασίας που θα λάβει τα δεδοµένα
tag είναι ένας αριθµός µε τον οποίο ο χρήστης χαρακτηρίζει το είδος του µηνύµατος
comm είναι ο communicator στις διεργασίες του οποίου θα αναζητηθεί η διεργασία που
θα λάβει τα δεδοµένα.
84
∆.Σ. Βλάχος και Θ.Η. Σίµος
Η συνάρτηση MPI_Recv
Int MPI_Recv (
void*
int
MPI_Datatype
int
int
MPI_Comm
MPI_Status*
Όπου:
message,
count,
datatype,
source,
tag,
comm.,
status)
message είναι ένας δείκτης σε µια περιοχή µνήµης στην οποία θα αποθηκευτούν τα
δεδοµένα που θα ληφθούν
count είναι ο αριθµός των στοιχείων που θα ληφθούν
datatype είναι ο τύπος των δεδοµένων
source είναι ο αριθµός της διεργασίας που στέλνει τα δεδοµένα,
tag είναι ένας αριθµός µε τον οποίο ο χρήστης χαρακτηρίζει το είδος των δεδοµένων
comm είναι ο communicator µέσα στις διεργασίες του οποίου γίνεται η επικοινωνία και
status είναι ένας δείκτης σε µια δοµή MPI_Status ή οποία µετά την επιστροφή της
συνάρτησης MPI_Recv περιέχει πληροφορίες για το αν η λήψη των στοιχείων έγινε
οµαλά ή άν υπήρχε κάποιο πρόβληµα τί είδους πρόβληµα ήταν αυτό.
Όταν µια διεργασία εκτελεί τη συνάρτηση MPI_Recv η λειτουργία της διακόπτεται µέχρι
να ληφθούν τα δεδοµένα που ζητήθηκαν. Η MPI_Recv µπορεί να επιστρέψει ακόµα και
όταν έχει γίνει κάποιο λάθος στην επικοινωνία. Στην περίπτωση αυτή, πληροφορίες για
το είδος του λάθους επιστρέφονται στη δοµή MPI_Status. Τα βασικά πεδία αυτής της
δοµής είναι τα:
status->MPI_source
τα δεδοµένα
status->MPI_tag
όπου αποθηκεύεται ο αριθµός της διεργασίας η οποία έστειλε
όπου αποθηκεύεται το tag του µηνύµατος που παραλήφθηκε και
status->MPI_ERROR
λήψη των στοιχείων.
όπου αποθηκεύεται ο κωδικός του λάθους που έγινε κατά την
Ο λόγος για τον οποίο η δοµή status επιστρέφει τον αριθµό της διεργασίας που έστειλε τα
δεδοµένα είναι γιατί η συνάρτηση MPI_Recv µπορεί να κληθεί µε την τιµή
MPI_ANY_SOURCE στη µεταβλητή source οπότε λαµβάνεται ένα µήνυµα ανεξάρτητα
από το ποια διεργασία το στέλνει.
85
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Τέλος ο λόγος που η δοµή status επιστρέφει την τιµή του tag του µηνύµατος που
παραλήφθηκε είναι γιατί η συνάρτηση MPI_Recv µπορεί να κληθεί µε την τιµή
MPI_ANY_TAG στη µεταβλητή tag όπότε λαµβάνεται ένα µήνυµα ανεξάρτητα από το
tag του.
Αδιέξοδο
Η χρήση των συναρτήσεων MPI_Send και MPI_Recv µπορεί να οδηγήσει σε µια
κατάσταση που λέγεται αδιέξοδο (deadlock). Στην περίπτωση αυτή µια διεργασία
περιµένει δεδοµένα που δεν έρχονται ποτέ γιατί η διεργασία που πρόκειται να τα στείλει
περιµένει και αυτή δεδοµένα που δεν έρχονται ποτέ κ.ο.κ. Αν συµβολίσουµε µε κόµβους
τις διεργασίες και σχεδιάσουµε µια ακµή από τον κόµβ0 –i στον κόµβο –j αν η διεργασία
–i περιµένει δεδοµένα από τον κόµβο –j, τότε το αδιέξοδο µπορεί να διαπιστωθεί µε την
ύπαρξη κλειστών διαδροµών στο γράφο που σχηµατίζεται.
Παρακάτω θα δούµε δύο πολύ συνηθισµένες περιπτώσεις που µπορεί να προκληθεί
αδιέξοδο. Οι διεργασίες 0 και 1 αποθηκεύουν η κάθε µία τις µεταβλητές a και b, και
πρέπει να ανταλλάξουν τα δεδοµένα τους για να υπολογίσουν τη µέση τιµή c=(a+b)/2.
Περίπτωση 1. Οι διεργασίες περιµένουν δεδοµένα πριν ακόµα τα στείλουν
float
a,b,c;
int
id;
MPI_Status status;
…
if (id==0) {
MPI_Recv(&b,1,MPI_FLOAT,1,0,MPI_COMM_WORLD,&status);
MPI_Send(&a,1,MPI_FLOAT,1,0,MPI_COMM_WORLD);
c=(a+b)/2.0;
}else if (id==1) {
MPI_Recv(&a,1,MPI_FLOAT,0,0,MPI_COMM_WORLD,&status);
MPI_Send(&b,1,MPI_FLOAT,0,0,MPI_COMM_WORLD);
c=(a+b)/2.0;
}
Περίπτωση 2. Οι διεργασίες στέλνουν τα δεδοµένα τους πριν µπουν στη διαδικασία να
περιµένουν αλλά τα tag των µηνυµάτων δε συµβαδίζουν:
float
a,b,c;
int
id;
MPI_Status status;
…
if (id==0) {
MPI_Recv(&b,1,MPI_FLOAT,1,1,MPI_COMM_WORLD,&status);
MPI_Send(&a,1,MPI_FLOAT,1,1,MPI_COMM_WORLD);
c=(a+b)/2.0;
}else if (id==1) {
MPI_Recv(&a,1,MPI_FLOAT,0,0,MPI_COMM_WORLD,&status);
MPI_Send(&b,1,MPI_FLOAT,0,0,MPI_COMM_WORLD);
c=(a+b)/2.0;
}
86
∆.Σ. Βλάχος και Θ.Η. Σίµος
Τέτοιου είδους προβλήµατα είναι πολύ συνηθισµένα στην ανάπτυξη παράλληλων
προγραµµάτων και η ανίχνευσή τους µπορεί να χρειαστεί πολλές ανθρωποώρες.
Το πλήρες πρόγραµµα
Παρακάτω παραθέτουµε το πλήρες πρόγραµµα για την υλοποίηση του αλγόριθµου του
Floyd.
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<mpi.h>
"mympi.h"
typedef int dtype;
#define MPI_TYPE MPI_INT
int main(int argc,char* argv[]) {
dtype**
a; // Doubly subscripted array
dtype*
storage;//Local portion of array elements
int
id;
//Process rank
int
m; //Rows
int
n; //Cols
int
p; //Number of processes
void compute_shortest_paths(int,int,int**,int);
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
read_row_striped_matrix(argv[1],(void*)&a,(void*)&storage
,MPI_TYPE,&m,&n,MPI_COMM_WORLD);
if (m!=n)
terminate(id,"Matrix must be square\n");
print_row_striped_matrix((void**)a,MPI_TYPE,m,n,MPI_COMM_
WORLD);
compute_shortest_paths(id,p,(dtype**)a,n);
print_row_striped_matrix((void**)a,MPI_TYPE,m,n,MPI_COMM_
WORLD);
MPI_Finalize();
}
void compute_shortest_paths(int id, int p, dtype** a,int n)
{
int
i,j,k;
int
offset;
int
root;
int*
tmp;
87
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
}
tmp=(dtype*)my_malloc(id,n*sizeof(dtype));
for (k=0;k<n;k++) {
root=BLOCK_OWNER(k,p,n);
if (root==id) {
offset=k-BLOCK_LOW(id,p,n);
for (j=0;j<n;j++)
tmp[j]=a[offset][j];
}
MPI_Bcast(tmp,n,MPI_TYPE,root,MPI_COMM_WORLD);
for (i=0;i<BLOCK_SIZE(id,p,n);i++) {
for (j=0;j<n;j++)
a[i][j]=MIN(a[i][j],a[i][k]+tmp[j]);
}
}
free(tmp);
Ανάλυση
Ο σειριακός αλγόριθµος του Floyd έχει πολυπλοκότητα Θ(n3). Ας δούµε τώρα την
πολυπλοκότητα του παράλληλου αλγόριθµου. Ο εσωτερικός βρόχος εκτελείται ακριβώς
όπως και στο σειριακό αλγόριθµο και έχει πολυπλοκότητα Θ(n). Ο µεσαίος βρόχος θα
εκτελεστεί το πολύ n/p φορές. Άρα η πολυπλοκότητα των δύο εσωτερικών βρόχων
είναι Θ(n2/p).
Πριν από το µεσαίο βρόχο υπάρχει η διαδικασία της µετάδοσης. Αφού η µετάδοση n
στοιχείων έχει πολυπλοκότητα Θ(n) και η µετάδοση σε p διεργασίες απαιτεί logp
βήµατα, η συνολική πολυπλοκότητα της µετάδοσης θα είναι Θ(nlogp).
Τέλος, ο εξωτερικός βρόχος θα εκτελεστεί n φορές. Κάθε φορά απαιτείται ο
προσδιορισµός της διεργασίας που έχει τη σωστή γραµµή του πίνακα (σταθερός χρόνος)
και ο προσδιορισµός του πίνακα tmp που έχει πολυπλοκότητα Θ(n). Έτσι, συνολικά η
πολυπλοκότητα του παράλληλου αλγόριθµου θα είναι:
Θ(n(1 + n + n log p + n 2 / p)) = Θ(n3 / p + n 2 log p)
Ας κάνουµε τώρα µια εκτίµηση για το χρόνο που απαιτείται για να τρέξει ο παράλληλος
αλγόριθµος. Αν λ είναι ο χρόνος που κάνει να µεταδοθεί η επικεφαλίδα ενός µηνύµατος
και β το εύρος ζώνης του καναλιού µετάδοσης, ο χρόνος για τη µετάδοση θα είναι:
n log p  (λ + 4n / β )
Αν χ τώρα είναι ο χρόνος για να ενηµερωθεί ένα κελί του πίνακα, ο συνολικός χρόνος θα
είναι:
n 2  n / p  χ + n  log p  (λ + 4n / β )
Πρέπει να σηµειώσουµε εδώ ότι η έκφραση που βγάλαµε είναι µια υπερεκτίµηση για το
χρόνο που κάνει να τρέξει ο παράλληλος αλγόριθµος. Ο λόγος είναι ότι αµελούµε το
88
∆.Σ. Βλάχος και Θ.Η. Σίµος
γεγονός ότι υπάρχει σηµαντική επικάλυψη υπολογιστικών και επικοινωνιακών
διαδικασιών. Αυτό φαίνεται στο παρακάτω σχήµα, όπου απεικονίζονται οι πρώτες 4
επαναλήψεις του αλγόριθµου.
Σχήµα 8. Κατά την εκτέλεση του παράλληλου αλγόριθµου του Floyd, υπάρχει σηµαντική
επικάλυψη µεταξύ της αποστολής ενός µηνύµατος και των υπολογισµών.
Λαµβάνοντας υπ’ όψιν το γεγονός αυτό, στο σχήµα 9 φαίνεται η θεωρητική και
πειραµατική καµπύλη απόδοσης του αλγόριθµου.
Σχήµα 9. Θεωρητική και πειραµατική καµπύλη απόδοσης του αλγόριθµου του Floyd.
89
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
90
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ανάλυση απόδοσης
Εισαγωγή
Είναι πολύ σηµαντικό να µπορούµε να προβλέψουµε την απόδοση ενός παράλληλου
αλγόριθµου πριν ακόµα ξεκινήσουµε τον προγραµµατισµό. Τα εργαλεία τα οποία θα
αναπτύξουµε για να κάνουµε κάτι τέτοιο εφικτό είναι:
•
Ο νόµος του Amdahl σύµφωνα µε τον οποίο µπορούµε να
αποφασίσουµε αν ένα πρόβληµα αξίζει να παραλληλοποιηθεί
•
Ο νόµος των Gustafson-Barsis µε τον οποίο µπορούµε να εκτιµήσουµε
την απόδοση ενός παράλληλου αλγόριθµου
•
Η µετρική Karp-Flatt µε την οποία µπορούµε να αποφασίσουµε αν το
φράγµα στην παραλληλοποίηση είναι ο σειριακός κώδικας ή επιπλέον
επικοινωνιακές καθυστερήσεις που εισάγει η παραλληλοποίηση
•
Η µετρική της ισο-απόδοσης σύµφωνα µε την οποία µπορούµε να
αποφασίσουµε αν η σχεδίασή µας αυξάνει την απόδοσή της µε τον
αριθµό των επεξεργαστών που χρησιµοποιούµε.
Επιτάχυνση και απόδοση
Όταν σχεδιάζουµε ένα παράλληλο πρόγραµµα περιµένουµε να τρέξει γρηγορότερα από
το αντίστοιχο σειριακό. Με τον όρο επιτάχυνση (speedup) εννοούµε το λόγο του χρόνου
που κάνει να τρέξει το σειριακό πρόγραµµα προς το χρόνο του αντίστοιχου παράλληλου:
Επιτ ά χυνση ( speedup ) =
Tseq
Tpar
όπου Tseq είναι ο σειριακός χρόνος και Tpar ο αντίστοιχος του παράλληλου
προγράµµατος.
Η εµπειρία µας µέχρι εδώ έχει δείξει ότι οι εργασίες που κάνει ένα παράλληλο
πρόγραµµα µπορούν να µπουν σε τρεις κατηγορίες:
1. Υπολογισµοί που γίνονται σειριακά
2. Υπολογισµοί που γίνονται παράλληλα
3. Επικοινωνίες και πράξεις αναγωγής
91
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Μπορούµε τώρα να αναπτύξουµε ένα απλό µοντέλο για την επιτάχυνση. Ας υποθέσουµε
ότι µε n συµβολίζουµε το µέγεθος του προβλήµατος και µε p τον αριθµό των
επεξεργαστών. Τότε, η επιτάχυνση ψ θα είναι µια συνάρτηση κει του µεγέθους του
προβλήµατος και του αριθµού των επεξεργαστών, δηλαδή ψ=ψ(n,p). Έστω τώρα σ(n)
είναι ο χρόνος που αντιστοιχεί σε εκείνο το τµήµα του προγράµµατος που πρέπει να
εκτελεστεί σειριακά, φ(n) ο χρόνος που αντιστοιχεί σε εκείνο το τµήµα του
προγράµµατος που µπορεί να παραλληλοποιηθεί και κ(n,p) ο επιπλέον χρόνος που
εισάγεται κατά την παραλληλοποίηση. Εύκολα µπορούµε να διαπιστώσουµε ότι ο χρόνος
που κάνει να τρέξει το σειριακό πρόγραµµα είναι:
Tseq = σ (n) + φ (n)
αφού στο σειριακό πρόγραµµα δεν υπάρχουν οι επικοινωνιακές καθυστερήσεις. Στην
καλύτερη περίπτωση, το παράλληλο πρόγραµµα θα τρέξει σε χρόνο:
Tpar ≥ σ (n) + φ (n) + κ (n, p)
p
όπου εδώ έχουµε υποθέσει ότι το τµήµα του κώδικα που µπορεί να παραλληλοποιηθεί
τρέχει σε κάθε επεξεργαστή µε ιδανικό τρόπο, δηλαδή χωρίς την ανάγκη συγχρονισµού
µεταξύ των επεξεργαστών.
Στη γενική περίπτωση τώρα, µπορούµε να υπολογίσουµε ένα άνω όριο για την
επιτάχυνση:
ψ (n, p) ≤
σ ( n ) + φ ( n)
σ ( n ) + φ ( n ) p + κ ( n, p )
Καθώς αυξάνουµε τον αριθµό των επεξεργαστών, µειώνεται ο χρόνος που αντιστοιχεί
στο τµήµα του προγράµµατος που µπορεί να παραλληλοποιηθεί αλλά αυξάνεται ο χρόνος
επικοινωνίας. Αυτό θα έχει σαν συνέπεια, µετά από κάποιο σηµείο, ο χρόνος που
καταναλώνεται για τις επικοινωνίες να είναι καθοριστικός. Το φαινόµενο αυτό φαίνεται
στο σχήµα 1.
92
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 1. Ένα παράλληλο πρόγραµµα έχει ένα τµήµα που ο χρόνος που καταναλώνει
µειώνεται µε τον αριθµό των επεξεργαστών (σκιασµένες µπάρες) και ένα τµήµα
(επικοινωνιακό µέρος) του οποίου ο χρόνος που καταναλώνει αυξάνεται µε τον αριθµό των
επεξεργαστών (λευκές µπάρες).
Με τον όρο απόδοση ε(n,p) ενός παράλληλου προγράµµατος εννοούµε το λόγο:
ε ( n, p ) =
Tseq
p ⋅ Tpar
Αντικαθιστώντας τις προηγούµενες σχέσεις, προκύπτει
σ ( n) + φ ( n)
p(σ (n) + φ (n) p + κ (n, p ))
σ ( n) + φ ( n)
⇒ ε ( n, p ) =
p ⋅ σ (n) + φ (n) + p ⋅ κ (n, p ))
ε ( n, p ) =
Εύκολα µπορούµε να διαπιστώσουµε ότι
0 ≤ ε (n, p) ≤ 1
93
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ο νόµος του Amdahl
Σύµφωνα µε τη σχέση που εξάγαµε για την επιτάχυνση και µε δεδοµένο το γεγονός ότι
κ(n,p)>0, έχουµε:
ψ (n, p) ≤
σ ( n ) + φ ( n)
σ ( n) + φ ( n ) p
Ας συµβολίσουµε τώρα µε f το λόγο του χρόνου που κάνει να τρέξει το τµήµα του
προγράµµατος που τρέχει σειριακά προς τον ολικό σειριακό χρόνο, δηλαδή
f =
σ ( n)
σ ( n) + φ ( n)
Τότε:
σ ( n) + φ ( n)
σ ( n) + φ ( n) / p
σ ( n) / f
⇒ ψ (n, p ) ≤
σ (n) + σ (n)(1/ f − 1) / p
ψ (n, p) ≤
1/ f
1 + (1/ f − 1) / p
1
⇒ ψ (n, p ) ≤
f + (1 − f ) / p
⇒ ψ (n, p ) ≤
Ο νόµος του Amdahl
Με δεδοµένο το κλάσµα του σειριακού κώδικα f, η µέγιστη επιτάχυνση ενός
παράλληλου προγράµµατος είναι:ψ ≤
1
f + (1 − f ) / p
Η χρήση του νόµου του Amdahl µπορεί να µας δώσει πληροφορίες για το πόσο
αποδοτικός µπορεί να είναι ένας παράλληλος αλγόριθµος. Ας δούµε µερικά
παραδείγµατα.
Παράδειγµα 1
Ας υποθέσουµε πως θέλουµε να εκτιµήσουµε αν αξίζει να αναπτύξουµε ένα παράλληλο
πρόγραµµα για τη λύση ενός προβλήµατος σε 8 επεξεργαστές. Μια πρόχειρη ανάλυση
που κάναµε έδειξε ότι το 90% του κώδικα µπορεί να εκτελεστεί παράλληλα. Ποια είναι η
µέγιστη επιτάχυνση που περιµένουµε;
•Λύση
Εύκολα διαπιστώνουµε ότι f=0.1. Έτσι, από το νόµο του Amdahl προκύπτει:
94
∆.Σ. Βλάχος και Θ.Η. Σίµος
ψ≤
1
≈ 4.7
0.1 + (1 − 0.1) / 8
Άρα, µπορούµε να περιµένουµε επιτάχυνση το πολύ 4.7
Παράδειγµα 2
Αν το 25% των υπολογισµών σε ένα πρόβληµα πρέπει να εκτελεστούν σειριακά, πόση
είναι η µέγιστη επιτάχυνση που µπορούµε να πετύχουµε;
•Λύση
Εύκολα διαπιστώνουµε ότι f=0.25. Έτσι, από το νόµο του Amdahl προκύπτει:
1
=4
p →∞ 0.25 + (1 − 0.25) / p
lim
Παράδειγµα 3
Ας υποθέσουµε ότι έχουµε φτιάξει ένα παράλληλο πρόγραµµα για την επίλυση ενός
προβλήµατος του οποίου η πολυπλοκότητα του σειριακού αλγόριθµου είναι Θ(n2).
Επίσης, ο χρόνος που χρειάζεται για τη σειριακή είσοδο και έξοδο των δεδοµένων είναι
(18000+n)µsec.
Ο χρόνος για την εκτέλεση του υπόλοιπου προγράµµατος παράλληλα είναι (n2/100)µsec.
Ποια είναι η µέγιστη επιτάχυνση που µπορούµε να πετύχουµε;
•Λύση
Από το νόµο του Amdahl έχουµε:
ψ≤
(28,000 + 1,000,000) µ sec
(28,000 + 1,000,000 / p) µ sec
Το παρακάτω σχήµα δείχνει την επιτάχυνση σαν συνάρτηση του p.
95
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 2. Η επιτάχυνση που προβλέπεται από τον νόµο του Amdhal (διακεκοµµένη γραµµή)
είναι µεγαλύτερη από αυτή που προβλέπεται αν λάβουµε υπ’ όψιν µας και το επικοινωνιακό
κόστος.
Ας δούµε τώρα ένα φαινόµενο το οποίο είναι γνωστό σαν φαινόµενο του Amdahl. Στη
γενική περίπτωση η πολυπλοκότητα του κ(n,p) είναι µικρότερη από αυτή του φ(n). Έτσι,
για δεδοµένο αριθµό επεξεργαστών, η αύξηση του µεγέθους του προβλήµατος θα έχει
σαν συνέπεια την αύξηση της επιτάχυνσης. Το φαινόµενο αυτό φαίνεται στο παρακάτω
σχήµα.
Σχήµα 3. Για έναν συγκεκριµένο αριθµό επεξεργαστών, η επιτάχυνση είναι αύξουσα
συνάρτηση του µεγέθους του προβλήµατος.
96
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ο νόµος των Gustafson και Barsis
Ενώ ο νόµος του Amdahl προσπαθεί να υπολογίσει πως θα αυξηθεί η επιτάχυνση ενός
παράλληλου αλγόριθµου µε την αύξηση του αριθµού των επεξεργαστών, θεωρώντας το
µέγεθος του προβλήµατος µια σταθερά, πολλές φορές χρειάζεται να κάνουµε ακριβώς το
αντίθετο, δηλαδή να θεωρούµε το χρόνο της εκτέλεσης σταθερό και να προσπαθούµε πως
θα αυξήσουµε το µέγεθος του προβλήµατος.
Στην περίπτωση αυτή, θα συµβολίσουµε µε s το κλάσµα του χρόνου που σε ένα
παράλληλο πρόγραµµα καταναλώνεται για την εκτέλεση του σειριακού κώδικα. Έτσι:
s=
σ (n)
σ ( n) + φ ( n ) / p
Η έκφραση για την επιτάχυνση παίρνει τώρα τη µορφή:
σ (n) = (σ (n) + φ (n) / p) s
φ (n) = (σ (n) + φ (n) / p )(1 − s) p
σ ( n) + φ ( n)
ψ ( n, p ) ≤
σ ( n) + φ ( n) / p
(σ (n) + φ (n) / p )( s + (1 − s ) p )
⇒ ψ ( n, p ) ≤
σ ( n) + φ ( n) / p
⇒ ψ (n, p ) ≤ s + (1 − s ) p
⇒ ψ (n, p ) ≤ p + (1 − p ) s
Ο νόµος των Gustafson και Barsis
Με δεδοµένο ότι το κλάσµα του χρόνου που καταναλώνει ένα παράλληλο
πρόγραµµα για την εκτέλεση του σειριακού κώδικα είναι s, η µέγιστη
επιτάχυνση που µπορεί να επιτευχθεί είναι:ψ ≤ p + (1 − p ) s
Παράδειγµα
Ας υποθέσουµε ότι έχουµε 16,384 επεξεργαστές και θέλουµε να πετύχουµε επιτάχυνση
15,000. Ποιο είναι το µέγιστο κλάσµα του σειριακού χρόνου του παράλληλου
προγράµµατος που θα τρέξει για να επιτευχθεί η παραπάνω επιτάχυνση;
•Λύση
Από το νόµο των Gustafson και Barsis προκύπτει:
15,000 ≤ 16,384 − 16,383s
⇒ s ≤ 1,384 / 16,383
⇒ s ≤ 0.084
97
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Η µετρική Karp-Flatt
Τόσο ο νόµος του Amdahl όσο και ο νόµος των Gustafson-Barsis αµελούν τον
επικοινωνιακό χρόνο κ(n,p) που εισάγει η παραλληλοποίηση ενός προγράµµατος. Ας
συµβολίσουµε µε T(n,p) το χρόνο που χρειάζεται ένα πρόγραµµα για να τρέξει σε p
επεξεργαστές. Τότε:
T (n, p) = σ (n) + φ (n) / p + κ (n, p)
Ο αντίστοιχος χρόνος για το σειριακό πρόγραµµα είναι:
T (n,1) = σ (n) + φ (n)
Ορίζουµε τώρα το πειραµατικά προσδιοριζόµενο σειριακό κλάσµα e σαν το κλάσµα
του χρόνου που καταναλώνει ένα παράλληλο πρόγραµµα σε λειτουργίες εκτός από αυτές
που έχουν παραλληλοποιηθεί (δηλαδή στο σειριακό τµήµα του κώδικα και στις
επικοινωνίες) προς το χρόνο που κάνει να τρέξει το πρόγραµµα σε έναν επεξεργαστή.
Έτσι:
e=
σ (n) + κ (n, p)
T (n,1)
Αφού η επιτάχυνση ψ=T(n,1)/T(n,p), και T(n,p)=T(n,1)e+T(n,1)(1-e)/p, µπορούµε να
γράψουµε:
T (n, p) = T (n, p )ψe + T (n, p)ψ (1 − e) / p ⇒
1 = ψe + ψ (1 − e) / p ⇒
1 / ψ = e + (1 − e) / p ⇒
1 /ψ = e + 1 / p − e / p ⇒
1 / ψ = e(1 − 1 / p) + 1 / p ⇒
1 /ψ − 1 / p
e=
1 − 1/ p
Η µετρική Karp-Flatt
Με δεδοµένο ότι ένα παράλληλο πρόγραµµα έχει επιτάχυνση ψ, το
πειραµατικά προσδιοριζόµενο σειριακό κλάσµα e είναι: e =
1 /ψ − 1 / p
1 − 1/ p
Παράδειγµα 1
Στον παρακάτω πίνακα φαίνεται η επιτάχυνση ενός παράλληλου προγράµµατος σαν
συνάρτηση του αριθµού των επεξεργαστών.
P
2
3
4
5
6
7
8
98
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ψ
1.82
2.50
3.08
3.57
4.00
4.38
4.71
Ποιος είναι ο λόγος που το παράλληλο πρόγραµµα παρουσιάζει επιτάχυνση µόλις 4.71
όταν τρέχει σε 8 επεξεργαστές;
•Λύση
Με τη χρήση της µετρικής Karp-Flatt υπολογίζουµε το πειραµατικά προσδιοριζόµενο
σειριακό κλάσµα:
P
2
3
4
5
6
7
8
Ψ
1.82
2.50
3.08
3.57
4.00
4.38
4.71
e
0.10
0.10
0.10
0.10
0.10
0.10
0.10
Παρατηρούµε ότι το e παραµένει σταθερό ενώ αυξάνουµε τον αριθµό των επεξεργαστών.
Αν θυµηθούµε τώρα τον ορισµό του e, θα δούµε ότι ο παρονοµαστής είναι ο σειριακός
χρόνος και άρα είναι σταθερός και από τον αριθµητή, η ποσότητα κ(n,p) ξέρουµε ότι
αυξάνει µε την αύξηση του p. Παρ’ όλ’ αυτά, πρέπει και ο αριθµητής να µένει σταθερός,
πράγµα που σηµαίνει ότι το σ(n) είναι πολύ µεγαλύτερο του κ(n,p). Εποµένως, ο λόγος
που το συγκεκριµένο πρόβληµα παρουσιάζει µικρή επιτάχυνση είναι ότι το τµήµα του
κώδικα που εκτελείται σειριακά είναι πολύ µεγάλο.
Παράδειγµα 2
Στον παρακάτω πίνακα φαίνεται η επιτάχυνση ενός παράλληλου προγράµµατος σαν
συνάρτηση του αριθµού των επεξεργαστών.
P
2
3
4
5
6
7
8
Ψ
1.87
2.61
3.23
3.73
4.14
4.46
4.71
Ποιος είναι ο λόγος που το παράλληλο πρόγραµµα παρουσιάζει επιτάχυνση µόλις 4.71
όταν τρέχει σε 8 επεξεργαστές;
•Λύση
Με τη χρήση της µετρικής Karp-Flatt υπολογίζουµε το πειραµατικά προσδιοριζόµενο
σειριακό κλάσµα:
P
2
3
4
5
6
7
8
Ψ
1.87
2.61
3.23
3.73
4.14
4.46
4.71
99
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
0.070
e
0.075
0.080
0.085
0.090
0.095
0.100
Παρατηρούµε ότι το e αυξάνει ενώ αυξάνουµε τον αριθµό των επεξεργαστών. Αυτό
σηµαίνει ότι ο αριθµητής στον ορισµό του e αυξάνει, πράγµα το οποίο οφείλεται στο
κ(n,p). Έτσι, ο επικοινωνιακός χρόνος είναι καθοριστικός σε αυτό το πρόβληµα και αυτός
είναι ο λόγος που παρατηρούµε χαµηλή επιτάχυνση.
Η µετρική ισο-απόδοσης
Είδαµε προηγουµένως ότι η απόδοση ενός παράλληλου προγράµµατος αυξάνει µε το
µέγεθος του προβλήµατος (το φαινόµενο Amdahl). ∆ε συµβαίνει όµως το ίδιο και µε την
αύξηση των επεξεργαστών. Μπορούµε λοιπόν να εξασφαλίσουµε ότι το πρόγραµµά µας
θα έχει σταθερή απόδοση όταν αυξάνουµε τον αριθµό των επεξεργαστών, αρκεί να
αυξάνουµε και το µέγεθος του προβλήµατος.
Για να εξάγουµε µια σχέση κατάλληλη για τον προσδιορισµό των συνθηκών για σταθερή
απόδοση, θα θεωρήσουµε τον υπολογιστικό χρόνο που καταναλώνεται από τους
επεξεργαστές ενός παράλληλου υπολογιστή που δεν υφίσταται σε έναν σειριακό. Κατ’
αρχήν, p επεξεργαστές καταναλώνουν υπολογιστικό χρόνο pκ(n,p) για τις επικοινωνίες.
Επιπλέον, όταν ο ένας από τους p επεξεργαστές εκτελεί το σειριακό κοµµάτι του κώδικα,
οι υπόλοιποι (p-1) επεξεργαστές καταναλώνουν αυτό το χρόνο χωρίς να κάνουν τίποτε.
Ορίζουµε λοιπόν τον επιπλέον υπολογιστικό χρόνο που δαπανάται σε έναν παράλληλο
υπολογιστή ως εξής:
T0 (n, p ) = ( p − 1)σ (n) + pκ (n, p )
Η σχέση τώρα για την επιτάχυνση µπορεί να γραφεί:
σ ( n) + φ ( n )
⇒
σ ( n ) + φ ( n ) / p + κ ( n, p )
p (σ (n) + φ (n))
ψ ( n, p ) ≤
⇒
pσ (n) + φ (n) + pκ (n, p)
p(σ (n) + φ (n))
ψ (n, p) ≤
⇒
σ (n) + φ (n) + ( p − 1)σ (n) + pκ (n, p )
p(σ (n) + φ (n))
ψ (n, p) ≤
σ (n) + φ (n) + T0 (n, p)
ψ ( n, p ) ≤
Αν θυµηθούµε τώρα ότι η απόδοση είναι η επιτάχυνση δια τον αριθµό των
επεξεργαστών, προκύπτει:
ε ( n, p ) ≤
1+
1
T0 (n, p )
T (n,1)
ή µετά από λίγες πράξεις:
100
∆.Σ. Βλάχος και Θ.Η. Σίµος
T (n,1) ≥
ε (n, p)
T 0(n, p )
1 − ε (n, p)
Αφού αυτό που θέλουµε είναι να έχουµε σταθερή απόδοση, η ποσότητα ε(n,p)/(1-ε(n,p))
πρέπει να παραµένει σταθερά. Έτσι:
T (n,1) ≥ CT0 (n, p) όπου
C=
ε (n, p)
1 − ε (n, p)
Η µετρική ισο-απόδοσης
Έστω ε(n,p) η απόδοση ενός παράλληλου προγράµµατος. Ορίζοντας τις
ποσότητες C και Τ0 όπως προηγουµένως, η συνθήκη για να έχει το
πρόγραµµα µας σταθερή απόδοση είναι T ( n,1) ≥ CT0 ( n, p )
Ας δούµε λίγο τώρα πως µπορούµε να χρησιµοποιήσουµε τη σχέση που εξάγαµε. Έστω
ότι η µετρική ισο-απόδοσης για ένα πρόβληµα έχει τη µορφή
n ≥ f ( p)
και M(n) είναι οι απαιτήσεις σε µνήµη του προβλήµατος. Τότε η σχέση
M −1 (n) ≥ f ( p)
δείχνει πως πρέπει να αυξάνει η µνήµη για να διατηρηθεί η σταθερή απόδοση. Σηµειώστε
εδώ ότι έχουµε αντικαταστήσει την έννοια του µεγέθους ενός προβλήµατος µε την
απαίτησή του σε µνήµη. Η παραπάνω σχέση µπορεί να γραφεί:
n ≥ M ( f ( p))
ή αν θεωρήσουµε ότι η συνολική µνήµη κατανέµεται οµοιόµορφα σε όλους τους
επεξεργαστές, η ποσότητα M(f(p))/p αντιστοιχεί στην απαιτούµενη µνήµη ανά
επεξεργαστή. Αυτή η ποσότητα ονοµάζεται και συνάρτηση κλιµάκωσης του
προβλήµατός µας.
Είναι σαφές τώρα πως η πολυπλοκότητα της συνάρτησης κλιµάκωσης είναι αυτή που
καθορίζει κατά πόσο το πρόβληµά µας µπορεί να διατηρήσει σταθερή απόδοση όπως
φαίνεται στο σχήµα 4.
101
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 4. Η πολυπλοκότητα της συνάρτησης κλιµάκωσης είναι ο λόγος που µπορεί να
υπάρχει ένα όριο στην επίτευξη σταθερής απόδοσης για έναν παράλληλο αλγόριθµο.
Παράδειγµα 1. Αναγωγή.
Ας δούµε λίγο το παράδειγµα της αναγωγής. Ο σειριακός αλγόριθµος έχει
πολυπλοκότητα Θ(n). Το στάδιο της αναγωγής έχει πολυπλοκότητα Θ(logp) εποµένως
T0(n,p)=Θ(plogp). Έτσι, η µετρική της ισο-απόδοσης έχει τη µορφή:
n ≥ Cp log p
Η µνήµη που απαιτείται είναι M(n)=n. Εποµένως, η συνάρτηση κλιµάκωσης είναι:
M (Cp log p ) / p = Cp log p / p = C log p
Το αποτέλεσµα υποδηλώνει πως η µνήµη ανά επεξεργαστή πρέπει να αυξάνει
προκειµένου να διατηρήσουµε σταθερή απόδοση. Ας δούµε λίγο γιατί συµβαίνει αυτό.
102
∆.Σ. Βλάχος και Θ.Η. Σίµος
Αν διπλασιάσουµε τον αριθµό των επεξεργαστών και το µέγεθος του προβλήµατος, τότε
η µνήµη ανά επεξεργαστή µένει σταθερή και ίση µε n/p. Ο αριθµός των βηµάτων
αναγωγής όµως έχει τώρα ελαφρώς αυξηθεί από logp σε log2p. Αυτό έχει σαν συνέπεια
να µειωθεί λίγο η απόδοση. Εποµένως, για να διατηρηθεί σταθερή η απόδοση, θα πρέπει
η µνήµη που καταναλώνεται ανά επεξεργαστή και άρα το µέγεθος του προβλήµατος να
αυξηθεί, όπως φαίνεται από τη συνάρτηση κλιµάκωσης.
Παράδειγµα 2. Ο αλγόριθµος του Floyd.
Η πολυπλοκότητα του σειριακού αλγόριθµου του Floyd είναι Θ(n3). Ο χρόνος που ο κάθε
επεξεργαστής δαπανά σε επικοινωνίες στον παράλληλο αλγόριθµο είναι Θ(n2logp).
Εποµένως, η µετρική ισο-απόδοσης είναι:
n3 ≥ C ( pn 2 log p) ⇒ n ≥ Cp log p
Η µνήµη που απαιτείται είναι Μ(n)=n2. Η συνάρτηση κλιµάκωσης παίρνει τη µορφή:
M (Cp log p) / p = C 2 p 2 log 2 p / p = C 2 p log 2 p
Προφανώς ο αλγόριθµος του Floyd έχει µικρότερες δυνατότητες κλιµάκωσης από το
πρόβληµα της αναγωγής.
Παράδειγµα 3. Πεπερασµένες διαφορές.
Ας θεωρήσουµε έναν παράλληλο αλγόριθµο για την επίλυση µιας διαφορικής εξίσωσης
σε ένα διδιάστατο πλέγµα. Κάθε επεξεργαστής είναι υπεύθυνος για (n/√p x n/√p) σηµεία
του πλέγµατος. Σε κάθε βήµα του αλγόριθµου, κάθε επεξεργαστής ανταλλάσσει στοιχεία
µε τους γειτονικούς του. Η διαδικασία αυτή έχει πολυπλοκότητα Θ(n/√p). Η
πολυπλοκότητα του σειριακού αλγόριθµου είναι Θ(n2). Η µετρική της ισο-απόδοσης
παίρνει τη µορφή:
n 2 ≥ Cp (n / p ) ⇒ n ≥ C p
Η µνήµη που απαιτείται είναι Μ(n)=n2. Εποµένως, η συνάρτηση κλιµάκωσης είναι:
M (C p ) / p = C 2 p / p = C 2
δηλαδή σταθερή. Αυτό σηµαίνει ότι το πρόβληµα έχει ιδανική συµπεριφορά όσον αφορά
τις δυνατότητες κλιµάκωσής του.
103
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
104
∆.Σ. Βλάχος και Θ.Η. Σίµος
Πολλαπλασιασµός
πίνακα µε διάνυσµα
Εισαγωγή
Ο πολλαπλασιασµός πίνακα µε διάνυσµα είναι ενσωµατωµένος σε έναν τεράστιο αριθµό
προβληµάτων. Όλες σχεδόν οι µέθοδοι επίλυσης γραµµικών συστηµάτων για παράδειγµα
εµπλέκουν πολλαπλασιασµούς πινάκων µε διανύσµατα. Στο κεφάλαιο αυτό θα
εξετάσουµε τρεις διαφορετικές µεθόδους για την παραλληλοποίηση τέτοιων
υπολογισµών. Οι τρεις αλγόριθµοι που θα αναπτύξουµε βασίζονται στον τρόπο µε τον
οποίο κατανέµονται τα στοιχεία του πίνακα και των διανυσµάτων ανάµεσα στις
διεργασίες. Έτσι, θα χρειαστούµε την εισαγωγή νέων συναρτήσεων MPI για να
υλοποιηθούν οι διάφορες επικοινωνίες µεταξύ των διεργασιών. Οι συναρτήσεις αυτές
είναι:
•
MPI_Allgatherv µε την οποία συγκεντρώνονται στοιχεία από διάφορες
διεργασίες όπου κάθε διεργασία συνεισφέρει διαφορετικό αριθµό στοιχείων
•
MPI_Scatterv µε την οποία διανέµουµε στοιχεία σε διαφορετικές διεργασίες
αλλά κάθε διεργασία λαµβάνει διαφορετικό αριθµό στοιχείων
•
MPI_Gatherv µε την οποία ο αριθµός των στοιχείων που συνεισφέρει η κάθε
διεργασία µπορεί να είναι διαφορετικός
Επιπλέον, για την υλοποίηση της γενικότερης µορφής τεµαχισµού (Καρτεσιανό πλέγµα)
θα χρειαστούµε τις συναρτήσεις:
•
MPI_Dims_create η οποία δηµιουργεί συντεταγµένες για ένα Καρτεσιανό
πλέγµα διεργασιών
•
MPI_Cart_create η οποία δηµιουργεί ένα Καρτεσιανό πλέγµα διεργασιών
•
MPI_Cart_coords η οποία επιστρέφει τις συντεταγµένες µιας διεργασίας σε ένα
Καρτεσιανό πλέγµα διεργασιών
•
MPI_Cart_rank η οποία επιστρέφει τον αριθµό µιας διεργασίας σε ένα
Καρτεσιανό πλέγµα διεργασιών
•
MPI_Comm_split η οποία χωρίζει τις διεργασίες ενός communicator σε
υποοµάδες
105
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ο σειριακός αλγόριθµος
Ο σειριακός αλγόριθµος για τον πολλαπλασιασµό ενός πίνακα µε ένα διάνυσµα φαίνεται
παρακάτω:
Είσοδος:
Έξοδος:
a[0..m-1,0..n-1]
b[0..n-1]
c[0..m-1]
for i=0 to m-1
c[i]=0
for j=0 to n-1
c[i]=c[i]+a[i,j]*b[j]
endfor
endfor
Όπως φαίνεται και στο σχήµα 1, κάθε στοιχείο του παραγόµενου διανύσµατος είναι ένα
εσωτερικό γινόµενο το οποίο χρειάζεται n πολλαπλασιασµούς και άρα έχει
πολυπλοκότητα Θ(n).
Σχήµα 1.Κάθε στοιχείο του παραγόµενου διανύσµατος είναι ένα εσωτερικό γινόµενο ενός
διανύσµατος γραµµή (γραµµή του πίνακα) µε ένα διάνυσµα στήλη.
Αφού έχουµε τελικά να υπολογίσουµε m στοιχεία, η συνολική πολυπλοκότητα του
σειριακού αλγόριθµου είναι Θ(mn).
Τεµαχισµός δεδοµένων
Όπως φαίνεται και στο σχήµα 2, υπάρχουν τρεις τρόποι µε τους οποίους µπορούµε να
τεµαχίσουµε τον αρχικό πίνακα: σε γραµµές, σε στήλες και σε ένα καρτεσιανό πλέγµα.
Έχουµε ήδη εξετάσει πως µπορούµε να τεµαχίσουµε έναν πίνακα κατά γραµµές από το
προηγούµενο κεφάλαιο. Σε αυτήν την περίπτωση η κάθε διεργασία θα είναι υπεύθυνη για
m/p το πολύ γραµµές του πίνακα.
Στον τεµαχισµό κατά στήλες, τα πράγµατα είναι ανάλογα, µόνο που τώρα κάθε διεργασία
είναι υπεύθυνη για n/p το πολύ στήλες του αρχικού πίνακα.
106
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 2. Τρεις διαφορετικοί τρόποι µε τους οποίους µπορούµε να τεµαχίσουµε τα στοιχεία
ενός πίνακα.
Στην τελευταία περίπτωση, έχουµε χωρίσει τις διεργασίες σε r γραµµές και c στήλες.
Έτσι, κάθε διεργασία θα είναι υπεύθυνη για ένα τµήµα του πίνακα το οποίο θα έχει το
πολύ m/r γραµµές και n/c στήλες.
Από την άλλη, υπάρχουν δύο τρόποι να κατανείµει κανείς τα στοιχεία των διανυσµάτων b
και c. Ο ένας τρόπος είναι να υπάρχουν σε όλες τις διεργασίες ενηµερωµένα αντίγραφα
των διανυσµάτων και ο άλλος κάθε διεργασία να έχει στην κατοχή της µόνο ένα τµήµα
µε το πολύ n/p στοιχεία.
Παρακάτω θα εξετάσουµε τρεις λύσεις για την παραλληλοποίηση του πολλαπλασιασµού
πίνακα µε διάνυσµα:
1. Τεµαχισµός του πίνακα κατά γραµµές και αντίγραφο των διανυσµάτων στις
διεργασίες
2. Τεµαχισµός του πίνακα κατά στήλες και τεµαχισµός των διανυσµάτων κατά τµήµατα
3. Τεµαχισµός του πίνακα σε καρτεσιανό πλέγµα και τεµαχισµός των διανυσµάτων
κατά τµήµατα.
Τεµαχισµός κατά γραµµές
Κάθε διεργασία είναι υπεύθυνη για ένα συγκεκριµένο αριθµό γραµµών του πίνακα a.
Επίσης, κάθε διεργασία έχει ένα αντίγραφο του διανύσµατος b και c. Στο σχήµα 3
φαίνεται πως υπολογίζεται το στοιχεί i του διανύσµατος c:
107
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 3. Κάθε αρχέγονη διεργασία έχει µια γραµµή του πίνακα a. Η διεργασία αυτή
υπολογίζει το εσωτερικό γινόµενο της γραµµής που διαθέτει µε το διάνυσµα b και παράγει
ένα στοιχείο του διανύσµατος c.
108
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ας υποθέσουµε για ευκολία ότι m-n. Τότε, κάθε διεργασία θα υπολογίσει m/p το πολύ
στοιχεία του διανύσµατος c. Η πολυπλοκότητα του παράλληλου αλγόριθµου θα είναι
τότε Θ(m2/p). Επίσης, τελικά θα χρειαστεί να συγκεντρωθούν τα στοιχεία του
διανύσµατος c που είναι κατανεµηµένα στις διεργασίες. Μια τέτοια λειτουργία ξέρουµε
ότι απαιτεί logp επικοινωνίες και τα συνολικά στοιχεία που θα µεταδοθούν θα είναι
n(p-1)/p (έχουµε θεωρήσει εδώ ότι το p είναι µια δύναµη του 2). Έτσι συνολικά η
επικοινωνία θα χρειάζεται χρόνο Θ(logp +n) και η συνολική πολυπλοκότητα του
παράλληλου αλγόριθµου θα είναι Θ(n2/p+n+logp).
Η πολυπλοκότητα του σειριακού αλγόριθµου είναι Θ(n2). Ο επικοινωνιακός χρόνος είναι
περίπου Θ(n), όπου έχουµε θεωρήσει ότι το n είναι πολύ µεγαλύτερο του logp. Η µετρική
ισο-απόδοσης παίρνει τη µορφή:
n 2 ≥ Cpn ⇒ n ≥ Cp
και επειδή Μ(n)=n2 η συνάρτηση κλιµάκωσης είναι:
M (Cp ) / p = C 2 p
πράγµα που σηµαίνει πως το µέγεθος του προβλήµατος πρέπει να αυξάνει γραµµικά µε
τον αριθµό των επεξεργαστών για να διατηρηθεί σταθερή η απόδοση.
Αντιγραφή κατανεµηµένου διανύσµατος
Μετά το τέλος των υπολογισµών, κάθε διεργασία έχει ένα τµήµα του διανύσµατος c.
Αυτό που θέλουµε τώρα είναι κάθε διεργασία να αποκτήσει ένα πλήρες αντίγραφο του
διανύσµατος c όπως φαίνεται στο σχήµα 4.
Σχήµα 4. Κάθε διεργασία µεταδίδει σε όλες τις άλλες το τµήµα του διανύσµατος που
διαθέτει και τελικά όλες οι διεργασίες αποκτούν ένα αντίγραφο του διανύσµατος.
109
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Εκείνο που πρέπει να γνωρίζουµε εδώ είναι ότι η κάθε διεργασία περιέχει διαφορετικό
αριθµό στοιχείων του διανύσµατος c όπως φαίνεται στο σχήµα 5.
Σχήµα 5. Κάθε διεργασία συµµετέχει µε διαφορετικό αριθµό στοιχείων για την αντιγραφή
του διανύσµατος.
Αυτή η λειτουργία επιτυγχάνεται µε τη χρήση της συνάρτησης MPI_Allgatherv.
Η συνάρτηση MPI_Allgatherv
int MPI_Allgatherv
(
void*
send_buffer,
int
send_cnt,
MPI_Datatype
send_type,
void*
recv_buffer,
int*
recv_cnt,
int*
recv_disp,
MPI_Datatype
recv_type,
MPI_Comm
comm)
Όπου
send_buffer είναι µια περιοχή µνήµης όπου αποθηκεύονται τα στοιχεία που θα
αποσταλούν
send_cnt είναι ο αριθµός των στοιχείων που θα αποσταλούν
send_type είναι ο τύπος των στοιχείων που θα αποσταλούν
recv_buffer είναι µια περιοχή της µνήµης στην οποία θα αποθηκευτούν τα δεδοµένα
που θα ληφθούν
recv_cnt είναι ένας πίνακας που το κάθε στοιχείο του είναι ο αριθµός των στοιχείων που
θα ληφθούν από κάθε διεργασία
110
∆.Σ. Βλάχος και Θ.Η. Σίµος
recv_disp είναι ένας πίνακας του οποίου κάθε στοιχείο δείχνει σε ποιο σηµείο του
πίνακα recv_buffer θα αποθηκευτούν τα στοιχεία που θα ληφθούν από κάθε διεργασία
recv_type είναι ο τύπος των δεδοµένων που θα ληφθούν
comm. Είναι ο communicator στα πλαίσια του οποίου θα γίνει η συλλογή των στοιχείων.
Στο παρακάτω σχήµα φαίνεται ένα παράδειγµα χρήσης της συνάρτησης MPI_Allgtherv.
Σχήµα 6. Η συνάρτηση MPI_Allgatherv συγκεντρώνει διαφορετικό αριθµό στοιχείων από
τις διεργασίες και το αποτέλεσµα το αντιγράφει σε όλες τις διεργασίες.
Ένα σηµείο το οποίο εισάγει µια δυσκολία στη χρήση της συνάρτησης MPI_Allgatherv
είναι η δηµιουργία των πινάκων που περιέχουν των αριθµό των στοιχείων που
συνεισφέρει η κάθε διεργασία και τις θέσεις που θα τοποθετηθούν τα στοιχεία κάθε
διεργασίας στο τελικό διάνυσµα. Αναπτύσσουµε λοιπόν παρακάτω δύο συναρτήσεις
γενικής χρήσης, την create_mxed_xfer_arrays και replicate_block_vector για να κάνουν
αυτές τις λειτουργίες:
// Function create_mixed_xfer_arrays.
// It creates the count and displacement arrays
// needed for scatter and gather operations.
void create_mixed_xfer_arrayes(
111
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
{
}
int
int
int
int**
int**
int
id,
//IN - Process id
p,
//IN - Number of processes
n,
//IN - Total number of elements
count,
//OUT - Array of counts
disp) //OUT - Array of displacements
i;
*count=my_malloc(id,p*sizeof(int));
*disp=my_malloc(id,p*sizeof(int));
(*count)[0]=BLOCK_SIZE(0,p,n);
(*disp)[0]=0;
for (i=1;i<p;i++) {
(*disp)[i]=(*disp)[i-1]+(*count)[i-1];
(*count)[i]=BLOCK_SIZE(i,p,n);
}
// Function replicate_block_vector.
// It replicates a distributed vector to all the
// processes in a communicator
void replicate_block_vector
(
void
*ablock, //IN - Block-distributed vector
int
n,
//IN - Elements in vector
void
*arep,
//OUT - Replicated vector
MPI_Datatype
dtype,
//IN - Element type
MPI_Comm comm)
//IN - Communicator
{
int*
cnt;
int*
disp;
int
id;
int
p;
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
crete_mixed_xfer_arrays(id,p,n,&cnt,&disp);
MPI_Allgatherv(ablock,cnt[id],dtype,arep,cnt,disp,dtype,c
omm);
free(cnt);
free(disp);
}
Επίσης, αναπτύσσουµε και δύο συναρτήσεις γενικής χρήσης για ανάγνωση και εκτύπωση
αντιγράφων διανυσµάτων:
// Function print_replicated_vector.
// Prints a vector that is replicated among the
// proccesses of a communicator
void print_replicated_vector (
void*
v,
//IN - Vector
MPI_Datatype
dtype,
//IN - Element type
112
∆.Σ. Βλάχος και Θ.Η. Σίµος
{
}
int
MPI_Comm
int
n,
comm)
//IN - Vector size
//IN - Communicator
id;
MPI_Comm_rank(comm,&id);
if (!id) {
print_subvector(v,dtype,n);
printf("\n\n");
}
// Function read_replicated_vector.
// It opens a file, reads the contents of a vector
// and replicates it among all the processes.
void read_replicated_vector
(
char*
s,
//IN - File name
void**
v,
//OUT - Vector
MPI_Datatype
dtype,
//IN - Vector type
int*
n,
//OUT - Vector length
MPI_Comm
comm)
//IN - Communicator
{
int
datum_size;
int
i;
int
id;
FILE*
infileptr;
int
p;
}
MPI_Comm_rank(comm,&id);
MPI_Comm_size(comm,&p);
datum_size=get_size(dtype);
if (id==(p-1)) {
infileptr=fopen(s,"r");
if (infileptr==NULL)
*n=0;
else
fread(n,sizeof(int),1,infileptr);
}
MPI_Bcast(n,1,MPI_INT,p-1,comm);
if (!*n)
terminate(id,"Cannot open vector file");
*v=my_malloc(id,*n*datum_size);
if (id==p-1)
{
fread(*v,datum_size,*n,infileptr);
fclose(infileptr);
}
MPI_Bcast(*v,*n,dtype,p-1,comm);
113
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Τέλος, ο κώδικας που υλοποιεί τον παράλληλο αλγόριθµο φαίνεται παρακάτω:
#include <stdio.h>
#include <mpi.h>
#include “mympi.h”
typedef fouble dtype;
#define mpitype MPI_DOUBLE
int main(int argc, char* argv[]) {
dtype
**a;
dtype
*b;
dtype
*c_block;
dtype
*c;
dtype
*storage;
int
i,j;
int
id;
int
m;
int
n;
int
nprime;
int
p;
int
rows;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
read_row_striped_matrix(argv[1],(void*)&a,
(void*)&storage, mpitype,&m,&n,MPI_COMM_WORLD);
rows=BLOCK_SIZE(id,p,n);
print_row_striped_matrix((void**)a,mpitype,m,n,
MPI_COMM_WORLD);
read_replicated_vector(argv[2],(void*)&b,mpitype,
(&nprime,MPI_COMM_WORLD);
print_replicated_vector(b,mpitype,nprime,
MPI_COMM_WORLD);
c_block=(dtype*)malloc(rows*sizeof(dtype));
c=(dtype*)malloc(n*sizeof(dtype));
for (i=0;i<rows;i++) {
c_block[i]=0.0;
for (j=0;j<n;j++)
c_block[j]+=a[i][j]*b[j];
}
replicate_block_vector(c_block,n,(void*)c,mpitype,
MPI_COMM_WORLD);
Print_replicated_vector(c,mpitype,n,MPI_COMM_WORLD);
114
∆.Σ. Βλάχος και Θ.Η. Σίµος
}
MPI_Finalize();
Return 0;
Ανάλυση χρόνου
Ας δούµε τώρα µια εκτίµηση για το χρόνο που κάνει να τρέξει ο παράλληλος αλγόριθµος.
Αν χ είναι ο χρόνος για τον υπολογισµό ενός γινοµένου, τότε ο χρόνος που διαρκεί το
υπολογιστικό µέρος του αλγόριθµου είναι χnn/p.
Η διαδικασία συγκέντρωσης των στοιχείων χρειάζεται logp βήµατα και ο συνολικός
αριθµός στοιχείων που µεταδίδονται είναι:
n(2 log p  − 1) / 2 log p 
Κάθε στοιχείο καταναλώνει 8 Bytes. Έτσι ο συνολικός χρόνος για την επικοινωνία θα
είναι:
λ log p  + 8n
2 log p  − 1
β 2 log p 
Ο παρακάτω πίνακας δείχνει τις πειραµατικές και θεωρητικές τιµές για την εφαρµογή του
αλγόριθµου σε ένα cluster.
Επεξεργαστές
Εκτιµώµενος χρόνος
Πειραµατικός χρόνος
Επιτάχυνση
1
0.0634
0.0634
1
2
0.0324
0.0327
1.94
3
0.0223
0.0227
2.79
4
0.0170
0.0178
3.56
5
0.0141
0.0152
4.16
6
0.0120
0.0133
4.76
7
0.0105
0.0122
5.19
8
0.0094
0.0111
5.70
16
0.0057
0.0072
8.79
Τεµαχισµός κατά στήλες
Ο υπολογισµός ενός στοιχείου του διανύσµατος c µπορεί να θεωρηθεί σαν ένα άθροισµα
n µερικών αθροισµάτων. Αν τώρα µια διεργασία έχει τα στοιχεία µιας στήλης του πίνακα
a, και πολλαπλασιάσει ένα προς ένα τα στοιχεία αυτά µε τα στοιχεία του διανύσµατος b
τότε θα προκύψουν για κάθε ένα από τα στοιχεία του διανύσµατος c ένα από τα µερικά
αθροίσµατα που τα συνθέτουν. Αυτό µπορούµε να το δούµε και ως εξής:
115
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI


[ci ] = ∑ aij ⋅ b j  = ∑ aij ⋅ b j

j

j
Σχήµα 7. Εδώ κάθε αρχέγονη διεργασία έχει µια στήλη του πίνακα a και ένα στοιχείο του
διανύσµατος b. Το αποτέλεσµα του πολλαπλασιασµού της στήλης µε το στοιχείο είναι ένα
διάνυσµα του οποίου κάθε στοιχείο είναι ένα από τα n µερικά αθροίσµατα που συνθέτουν
το διάνυσµα c.
όπου το διάνυσµα [ajibj] έχει σαν στοιχεία το γινόµενο (ένα προς ένα) των στοιχείων της ι
γραµµής του πίνακα a µε το διάνυσµα b.
116
∆.Σ. Βλάχος και Θ.Η. Σίµος
Στη συνέχεια, για να αποκτήσουν όλες οι διεργασίες ένα αντίγραφο του διανύσµατος c θα
πρέπει να γίνει µια καθολική επικοινωνία.
Ας υποθέσουµε τώρα ότι m=n. Κάθε διεργασία είναι υπεύθυνη για n/p το πολύ µερικά
αθροίσµατα και ο υπολογισµός θα έχει πολυπλοκότητα Θ(n2/p). Η πρόσθεση των n/p
µερικών αθροισµάτων που έχει η κάθε διεργασία θα έχει πολυπλοκότητα Θ(n).
Η καθολική επικοινωνία µπορεί να γίνει µε την τοπολογία του υπερ-κύβου. Κάθε
διεργασία στέλνει σε κάθε βήµα n/2 στοιχεία και λαµβάνει n/2. Ο συνολικός αριθµός των
στοιχείων είναι nlogp. Έτσι, η συνολική πολυπλοκότητα του αλγόριθµου είναι
Θ(n2/p+nlogp).
Ανάγνωση πίνακα κατά στήλες
Παρακάτω δίνουµε µια γενική συνάρτηση για την ανάγνωση πίνακα κατά στήλες:
// Function read_col_striped_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of columns
// to the MPI processes.
void read_col_striped_matrix (
char*
s,
//IN - File name
void***
subs,
//OUT - 2D submatrix indices
void**
storage, //OUT - Submatrix storage area
MPI_Datatype dtype,
//IN - Matrix element type
int*
m,
//OUT - Matrix rows;
int*
n,
//OUT - Matrix cols
MPI_Comm comm)
//IN - Communicator
{
}
// Function read_row_striped_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of rows
// to the MPI processes.
void read_row_striped_matrix(
char*
s,
//IN - File name
void***
subs,
//OUT - 2D submatrix indices
void**
storage, //OUT - Submatrix storage area
MPI_Datatype dtype,
//IN - Matrix element type
int*
m,
//OUT - Matrix rows;
int*
n,
//OUT - Matrix cols
117
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
{
MPI_Comm comm)
//IN - Communicator
int
datum_size; //Size of matrix elements
int
i;
int
id;
//Process rank
FILE*
infileptr; //Input file pointer
int
local_rows; //Rows on this process
int
p;
//Number of processes
MPI_Status status;
//Result of receive
int
x;
//Result of read
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
datum_size=get_size(dtype);
//Process (p-1) opens file and reads matrix dimensions
if (id==p-1)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*m=0;
else {
fread(m,sizeof(int),1,infileptr);
fread(n,sizeof(int),1,infileptr);
}
}
MPI_Bcast(m,1,MPI_INT,p-1,comm);
//Check for abort
if(!(*m))
MPI_Abort(MPI_COMM_WORLD,OPEN_FILE_ERROR);
//Ok. Broadcast columns
MPI_Bcast(n,1,MPI_INT,p-1,comm);
//
local_rows=BLOCK_SIZE(id,p,*m);
//Dynamically allocate matrix
*storage=(void*)my_malloc(id,local_rows*(*n)*datum_size);
*subs=(void**)my_malloc(id,local_rows*PTR_SIZE);
for(i=0;i<local_rows;i++)
(*subs)[i]=&(((char*)(*storage))[i*(*n)*datum_size]);
//Process (p-1) reads a block and transmits it
if(id==p-1) {
for(i=0;i<p-1;i++)
{
x=fread(*storage,datum_size,BLOCK_SIZE(i,p,*m)*(*n),infil
eptr);
MPI_Send(*storage,BLOCK_SIZE(i,p,*m)*(*n),dtype,i,DATA_MS
G,comm);
}
118
∆.Σ. Βλάχος και Θ.Η. Σίµος
x=fread(*storage,datum_size,local_rows*(*n),infileptr);
fclose(infileptr);
}
else
MPI_Recv(*storage,local_rows*(*n),dtype,p1,DATA_MSG,comm,&status);
}
Η λειτουργία της συνάρτησης read_col_striped_matrix φαίνεται στο παρακάτω σχήµα:
Σχήµα 8. Η συνάρτηση read_col_striped_matrix διαβάζει µια γραµµή του πίνακα και
διανέµει τα στοιχεία της κατάλληλα στις διάφορες διεργασίες, ώστε τελικά κάθε διεργασία
να αποκτήσει ένα σύνολο στηλών του πίνακα.
Η συνάρτηση MPI_Scatterv
int Scatterv (
void*
int*
int*
MPI_Datatype
void*
int
MPI_Datatype
Int
MPI_COMM
Όπου
send_buffer,
send_cnt,
send_disp,
send_type,
recv_buffer,
recv_cnt,
recv_type,
root,
comm)
send_buffer είναι µια περιοχή µνήµης στην οποία αποθηκεύονται τα στοιχεία που
πρέπει να αποθηκευτούν
119
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
send_cnt είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει πόσα στοιχεία θα σταλούν
στην αντίστοιχη διεργασία
send_disp είναι ένας πίνακας του οποίου κάθε στοιχείο δείχνει τη θέση µέσα στον
πίνακα send_buffer του τµήµατος των δεδοµένων που θα σταλούν σε µια διεργασία
send_type είναι ο τύπος των δεδοµένων που θα σταλούν
recv_buffer είναι µια περιοχή της µνήµης στην οποία θα αποθηκευτούν τα δεδοµένα
που θα ληφθούν
recv_cnt είναι το πλήθος των δεδοµένων που θα ληφθούν
recv_type είναι ο τύπος των δεδοµένων που θα ληφθούν
root είναι ο αριθµός της διεργασίας που θα στείλει τα στοιχεία
comm είναι ο communicator στα πλαίσια του οποίου θα γίνει η επικοινωνία.
Ένα παράδειγµα της χρήσης της συνάρτησης MPI_Scatterv φαίνεται στο παρακάτω
σχήµα:
Σχήµα 9. Η συνάρτηση MPI_Scatterv διανέµει τα στοιχεία ενός διανύσµατος µε
διαφορετικό τρόπο ανάµεσα στις διεργασίες.
Η συνάρτηση MPI_Gatherv
int MPI_Gatherv
(
void*
send_buffer,
int
send_cnt,
MPI_Datatype
send_type,
void*
recv_buffer,
int*
recv_cnt,
120
∆.Σ. Βλάχος και Θ.Η. Σίµος
int*
recv_disp,
MPI_Datatype
recv_type,
int
root,
MPI_COMM comm)
όπου
send_buffer είναι µια περιοχή µνήµης στην οποία κάθε διεργασία αποθηκεύει τα
δεδοµένα που θα στείλει
send_cnt είναι ο αριθµός των στοιχείων που θα αποσταλούν (για κάθε διεργασία µπορεί
να είναι διαφορετικός)
send_type είναι ο τύπος των δεδοµένων που θα αποσταλούν
recv_buffer είναι µια περιοχή µνήµης στην οποία θα αποθηκευτούν τα στοιχεία που θα
ληφθούν
recv_cnt είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει πόσα στοιχεία έχουν
ληφθεί από την αντίστοιχη διεργασία
recv_disp είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει τη θέση στον πίνακα
recv_buffer που θα αποθηκευτούν τα δεδοµένα από την αντίστοιχη διεργασία
recv_type είναι ο τύπος των δεδοµένων που θα ληφθούν
root είναι ο αριθµός της διεργασίας που συλλέγει τα στοιχεία
comm είναι ο communicator στα πλαίσια του οποίου θα γίνει η επικοινωνία.
Ένα παράδειγµα της χρήσης της συνάρτησης MPI_Gatherv φαίνεται στο παρακάτω
σχήµα:
Σχήµα 10. Η συνάρτηση MPI_Gatherv συλλέγει διαφορετικό αριθµό στοιχείων από τις
διάφορες διεργασίες για τη δηµιουργία ενός διανύσµατος.
121
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Παρακάτω δίνουµε µια γενική συνάρτηση για την εκτύπωση των στοιχείων ενός πίνακα ο
οποίος έχει τεµαχιστεί ανάµεσα στις διεργασίες κατά στήλες:
void print_col_striped_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
comm)
//IN - Communicator
{
}
// Function print_checkboard_matrix
// Prints the contents of a matrix that is
// distributed checkboard fashion among the processes
void print_checkboard_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
grid_comm) //IN - Communicator
{
void*
buffer;
int
coords[2];
int
datum_size;
int
els;
int
grid_coords[2];
int
grid_id;
int
grid_period[2];
int
grid_size[2];
int
i,j,k;
void*
laddr;
int
local_cols;
int
p;
int
src;
MPI_Status status;
MPI_Comm_rank(grid_comm,&grid_id);
MPI_Comm_size(grid_comm,&p);
datum_size=get_size(dtype);
MPI_Cart_get(grid_comm,2,grid_size,grid_period,grid_coord
s);
local_cols=BLOCK_SIZE(grid_coords[1],grid_size[1],n);
if (!grid_id)
buffer=my_malloc(grid_id,n*datum_size);
// For each row in the process grid..
for (i=0;i<grid_size[0],i++) {
coords[0]=i;
// For each matrix row in the current process
122
∆.Σ. Βλάχος και Θ.Η. Σίµος
for (j=0;j<BLOCK_SIZE(i,grid_size[0],m);j++) {
// Collect the matrix row int the process 0
// and then print it
if (!grid_id) {
for (k=0;k<grid_size[1];k++) {
coords[1]=k;
MPI_Cart_rank(grid_comm,coords,&src);
els=BLOCK_SIZE(k,grid_size[1],n);
laddr=buffer+BLOCK_LOW(k,grid_size[1],n)*datum_size;
if (src==0) {
memcpy(laddr,a[j],els*datum_size);
}else {
}
MPI_Recv(laddr,els,dtype,src,0,grid_comm,&status);
}
}
print_subvector(buffer,dtype,n);
putchar('\n');
}else if (grid_coords[0]==i) {
MPI_Send(a[j],local_cols,dtype,0,0,grid_comm);
}
}
}
if (!grid_id) {
free(buffer);
putchar('\n');
}
Συλλογή στοιχείων
Μετά το τέλος των υπολογισµών, κάθε διεργασία θα έχει το πολύ n/p µερικά
αθροίσµατα. Έτσι, αφού κάθε διεργασία υπολογίσει το άθροισµα των στοιχείων που
διαθέτει, θα πρέπει να συµµετέχει σε µια καθολική επικοινωνία. Αυτό θα γίνει µε τη
χρήση της συνάρτησης MPI_Alltoallv η οποία υποχρεώνει κάθε διεργασία να ανταλλάξει
στοιχεία µε κάθε άλλη διεργασία.
Η συνάρτηση MPI_Alltoallv
int MPI_Alltoallv
void*
int*
int*
MPI_Datatype
void*
int*
int*
(
send_buffer,
send_count,
send_disp,
send_type,
recv_buffer,
recv_count,
recv_disp,
123
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Datatype
MPI_COMM
όπου
recv_type,
comm)
send_buffer είναι µια περιοχή µνήµης που κάθε διεργασία αποθηκεύει τα δεδοµένα που
θα ανταλλάξει
send_count είναι ένας πίνακας κάθε στοιχείο i του οποίου δείχνει των αριθµό των
στοιχείων που διαθέτει για ανταλλαγή η κάθε διεργασία και προορίζονται για τη
διεργασία i
send_disp είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει τη θέση στον πίνακα
send_buffer των στοιχείων που προορίζονται για την αντίστοιχη διεργασία
send_type είναι ο τύπος των στοιχείων που θα αποσταλούν
recv_buffer είναι µια περιοχή της µνήµης στην οποία η κάθε διεργασία θα αποθηκεύσει
τα στοιχεία που λαµβάνει
recv_count είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει πόσα στοιχεία
αναµένονται από την αντίστοιχη διεργασία
recv_disp είναι ένας πίνακας κάθε στοιχείο του οποίου δείχνει τη θέση στον πίνακα
recv_buffer που θα αποθηκευτούν τα στοιχεία που θα ληφθούν από την αντίστοιχη
διεργασία
recv_type είναι ο τύπος των δεδοµένων που θα ληφθούν
comm είναι ο communicator οι διεργασίες του οποίου συµµετέχουν στην ανταλλαγή.
Ένα παράδειγµα χρήσης της συνάρτησης MPI_Alltoallv φαίνεται στο παρακάτω σχήµα.
Σχήµα 11. Η συνάρτηση MPI_Alltoallv ανταλλάσσει διαφορετικό αριθµό στοιχείων µεταξύ
των διεργασιών.
Τέλος, ο πλήρης κώδικας του παράλληλου αλγόριθµου φαίνεται παρακάτω:
124
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <stdio.h>
#include <mpi.h>
#include “mympi.h”
typedef double dtype;
#define mpitype MPI_DOUBLE
int main(int argc, char* argv[])
dtype
**a;
dtype
*b;
dtype
*c;
dtype
*c_part_out;
dtype
*c_part_in;
int
*cnt_out;
int
*cnt_in;
int
*disp_out;
int
*disp_in;
int
i,j;
int
id;
int
local_els;
int
m;
int
n;
int
nprime;
int
p;
dtype
*storage;
[
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&id);
MPI_Comm_size(MPI_COMM_WORLD,&p);
read_col_striped_matrix(argv[1], (void***)&a,
(void**)&storage,mpitype,&m,&n,MPI_COMM_WORLD);
print_col_striped_matrix((void**)a,mpitype,m,n,
MPI_COMM_WORLD);
read_block_vector(argv[2],(void**)&b,mpitype,
&nprime,MPI_COMM_WORLD);
print_block_vector((void*)b,mpitype,nprime,
MPI_COMM_WORLD);
c_part_out=(dtype*)my_malloc(id,n*sizeof(dtype));
local_els=BLOCK_SIZE(id,p,n);
for (i=0;i<n;i++) {
c_part_out[i]=0.0;
for (j=0;j<local_els;j++)
c_part_out[i]+=a[i][j]*b[j];
}
cretate_mixed_xfer_arrays(id,p,n,&cnt_out,&disp_out);
125
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
create_uniform_xfer_arrays(id,p,n,&cnt_in,&disp_in);
c_part_in=(dtype*)my_malloc(id,
p*local_els*sizeof(dtype));
MPI_Alltoallv(c_part_out,cnt_out,disp_out,mpitype,
C_part_in,cnt_in,disp_in,mpitype,MPI_COMM_WORLD);
c=(dtype*)my_malloc(id,local_els*sizeof(dtype));
for (i=0;i<local_els;i++) {
c[i]=0.0;
for (j=0;j<p;j++)
c[i]+=c_part_in[i+j*local_els];
}
}
print_block_vector((void*)c,mpitype,n,MPI_COMM_WORLD);
MPI_Finalize();
Return 0;
Τεµαχισµός Καρτεσιανού πλέγµατος
Ας υποθέσουµε τώρα ότι σε κάθε αρχέγονη διεργασία αντιστοιχίζουµε το στοιχείο aij του
πίνακα και το στοιχείο bj του διανύσµατος. Το γινόµενο dij=aijbj σρησιµοποιείται για τον
υπολογισµό του στοιχείου
ci = ∑ di , j
j
Συγκεντρώνουµε τώρα τις αρχέγονες διεργασίες σε ορθογώνια τµήµατα, δηµιουργώντας
έτσι ένα πλέγµα διεργασιών. Κάθε διεργασία έχει περίπου τον ίδιο αριθµό αρχέγονων
διεργασιών για να έχουµε οµοιόµορφη κατανοµή του υπολογιστικού φόρτου. Το
διάνυσµα b κατανέµεται κατά τµήµατα στις διεργασίες της πρώτης στήλης του πλέγµατος
των διεργασιών όπως φαίνεται στο παρακάτω σχήµα.
Σχήµα 12. Το διάνυσµα b κατανέµεται κατά τµήµατα στις διεργασίες της πρώτης στήλης.
Στο παρακάτω σχήµα φαίνονται τα βήµατα που πρέπει να ακολουθήσουµε για να
υλοποιηθεί ο πολλαπλασιασµός:
126
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 13. Τα τµήµατα του διανύσµατος b διανέµονται στις υπόλοιπες διεργασίες του
πλέγµατος. Στη συνέχεια, αφού υπολογιστούν τοπικά τα γινόµενα, τα αποτελέσµατα
συλλέγονται κατά γραµµές στις διεργασίες της πρώτης στήλης του πλέγµατος.
Ας δούµε τώρα πως γίνεται η κατανοµή των στοιχείων του διανύσµατος b. Όταν το
πλέγµα είναι τετραγωνικό η διαδικασία είναι απλή. Αρκεί η διεργασία i της πρώτης
στήλης του πλέγµατος των διεργασιών να στείλει το τµήµα που διαθέτει στη διεργασία i
της πρώτης γραµµής του πλέγµατος. Έπειτα, κάθε διεργασία της πρώτης γραµµής του
πλέγµατος των διεργασιών στέλνει το τµήµα του διανύσµατος που έχει σε όλες τις
διεργασίες της ίδιας στήλης.
Στην περίπτωση που το πλέγµα δεν είναι τετραγωνικό τα πράγµατα είναι λίγο πιο
περίπλοκα. Αρχικά, η πρώτη διεργασία της πρώτης στήλης του πλέγµατος των
διεργασιών επανασυνθέτει το διάνυσµα b. Στη συνέχεια, µοιράζει τµήµατα του
διανύσµατος στις διεργασίες της πρώτης γραµµής του πλέγµατος και τελικά κάθε
διεργασία της πρώτης γραµµής του πλέγµατος των διεργασιών στέλνει το τµήµα του
διανύσµατος που έχει σε όλες τις διεργασίες της ίδιας στήλης.
Οι δύο αυτοί τρόποι διανοµής του διανύσµατος b φαίνονται στο παρακάτω σχήµα:
127
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 13. ∆ιανοµή του διανύσµατος b στην περίπτωση τετραγωνικού (a) και µη
τετραγωνικού (b) πλέγµατος διεργασιών.
Σηµειώστε ότι κάθε διεργασία της ίδιας στήλης του πλέγµατος των διεργασιών πρέπει να
έχει το ίδιο τµήµα του διανύσµατος.
Ας δούµε τώρα την πολυπλοκότητα του αλγόριθµου. Κάθε διεργασία είναι υπεύθυνη για
ένα τµήµα του πίνακα a µε διάσταση το πολύ n/√pxn/√p. Έτσι ο υπολογιστικός
χρόνος θα είναι Θ(n2/p).
Για την κατανοµή του διανύσµατος b και την ανακατασκευή του διανύσµατος c θα
χρειαστούµε χρόνο Θ(nlogp/√p). Έτσι, η συνολική πολυπλοκότητα είναι Θ(n2/p
+nlogp/√p).
128
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ο επιπλέον επικοινωνιακός χρόνος είναιQ
p ⋅ n log p / p = n log p p
Αφού M(n)=n2 η µετρική της ισο-απόδοσης είναι:
n 2 ≥ Cn p log p ⇒ n ≥ C p log p
Η συνάρτηση κλιµάκωσης θα είναι τώρα:
M (C p log p ) / p = C 2 p log 2 p / p = C 2 log 2 p
Η ικανότητα κλιµάκωσης αυτού του αλγόριθµου είναι σαφώς ανώτερη από αυτή των δύο
προηγούµενων µεθόδων.
∆ηµιουργία communicator
Όπως είδαµε στην προηγούµενη παράγραφο, θα χρειαστούµε τις ακόλουθες επικοινωνίες:
•
Οι διεργασίες της πρώτης στήλης του πλέγµατος των διεργασιών θα πρέπει να
συµµετάσχουν στη συλλογή των τµηµάτων του διανύσµατος b.
•
Οι διεργασίες της πρώτης γραµµής του πλέγµατος των διεργασιών θα πρέπει να
συµµετάσχουν στη διανοµή του διανύσµατος b.
•
Οι διεργασίες της ίδιας στήλης του πλέγµατος των διεργασιών θα πρέπει να
συµµετάσχουν στην αντιγραφή του τµήµατος του διανύσµατος b.
•
Οι διεργασίες της ίδιας γραµµής του πλέγµατος των διεργασιών θα πρέπει να
συµµετάσχουν στην αναγωγή των τµηµάτων του διανύσµατος c.
Προφανώς, εκτός του γενικού communicator MPI_COMM_WORLD θα χρειαστούµε
και άλλες οµαδοποιήσεις (communicators) των διεργασιών.
Η συνάρτηση MPI_Dims_create
Θέλουµε να δηµιουργήσουµε κατ’ αρχήν ένα πλέγµα από διεργασίες. Για να το
πετύχουµε αυτό πρέπει να καθορίσουµε τον αριθµό των διαστάσεων του πλέγµατος (π.χ
στην περίπτωσή µας έχουµε δύο διαστάσεις). Η συνάρτηση MPI_Dims_create κάνει αυτή
τη δουλειά για µας:
int MPI_Dims_create
int
nodes,
int
dims,
int*
size)
όπου
(
129
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
nodes είναι ο συνολικός αριθµός των διεργασιών στο πλέγµα
dims είναι ο αριθµός των διαστάσεων του πλέγµατος
size είναι ένας πίνακας µε τόσες θέσεις όσες ο αριθµός των διαστάσεων και κάθε
στοιχείο του πίνακα παίρνει σαν τιµή των αριθµό των διεργασιών στην αντίστοιχη
διάσταση. Αν η θέση ενός στοιχείου του πίνακα είναι 0, τότε η συνάρτηση καθορίζει τον
αριθµό των διεργασιών στην αντίστοιχη διάσταση, αλλιώς, αν σε µια θέση του πίνακα
έχουµε µη µηδενική θετική τιµή, σηµαίνει πως η αντίστοιχη διάσταση έχει προκαθοριστεί
από το χρήστη.
Στην περίπτωσή µας που έχουµε p διεργασίες και θέλουµε διδιάστατο πλέγµα, η κλήση
θα γίνει ως εξής:
int
p;
int
size[2];
size[0]=size[1]=0;
MPI_Dims_create(p,2,size);
Με την επιστροφή της συνάρτησης το size[0] θα περιέχει τον αριθµό των γραµµών του
πλέγµατος και το size[1] τον αριθµό των στηλών.
Στο σηµείο αυτό θα δηµιουργήσουµε έναν communicator που θα σχετίζεται µε αυτήν την
τοπολογία (διδιάστατο πλέγµα). Αυτό θα το πετύχουµε µε τη συνάρτηση
MPI_Cart_create.
HΗ συνάρτηση MPI_Cart_create
int MPI_Cart_create (
MPI_Comm
old_comm,
int
dims,
int
*size,
int
*periodic,
int
reorder,
MPI_Comm
*cart_comm)
όπου
old_comm είναι ο παλιός communicator
dims είναι ο αριθµός των διαστάσεων του πλέγµατος
size είναι ένας πίνακας που έχει τον αριθµό των διεργασιών σε κάθε διάσταση
periodic είναι ένας πίνακας που κάθε του στοιχείο αν είναι 1 δείχνει αν οι διεργασίες
στην αντίστοιχη διάσταση θα είναι περιοδικά κατανεµηµένες (η τελευταία ενώνεται µε
την πρώτη) ή αν είναι 0 δείχνει ότι οι διεργασίες στην αντίστοιχη διάσταση δε θα είναι
περιοδικά κατανεµηµένες
reorder είναι ένας διακόπτης που αν είναι 0 οι αριθµοί των διεργασιών παραµένουν ως
είχαν στον παλιό communicator
130
∆.Σ. Βλάχος και Θ.Η. Σίµος
cart_comm είναι ένας δείκτης στο σηµείο που θα αποθηκευτεί ο νέος communicator
Ας δούµε λίγο τη χρήση αυτής της συνάρτησης στην περίπτωσή µας:
MPI_Comm
cart_comm;
int
p;
int
periodic[2];
int
size[2];
:
size[0]=size[1]=0;
MPI_Dims_create(p,2,size);
periodic[0]=periodic[1]=0;
MPI_Cart_create(MPI_COMM_WORLD,2,size,
periodic,1,&cart_comm);
Ανάγνωση πίνακα κατανεµηµένου σε πλέγµα
Έστω ότι η διεργασία 0 είναι υπεύθυνη για την ανάγνωση του πίνακα. Κάθε φορά που
διαβάζει µια γραµµή του πίνακα, τη στέλνει στην πρώτη διεργασία της αντίστοιχης
γραµµής του πλέγµατος των διεργασιών. Η διεργασία αυτή µε τη σειρά της στέλνει
τµήµατα αυτής της γραµµής στις διεργασίες της ίδιας γραµµής του πλέγµατος των
διεργασιών. Η τεχνική αυτή φαίνεται στο παρακάτω σχήµα:
Σχήµα 14. Ανάγνωση πίνακα κατανεµηµένου σε πλέγµα.
131
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Για την υλοποίηση της παραπάνω τεχνικής θα χρειαστούµε τις ακόλουθες συναρτήσεις:
Η συνάρτηση MPI_Cart_rank
Η συνάρτηση MPI_Cart_rank χρησιµοποιείται για να βρούµε τον αριθµό µιας διεργασίας
από τις συντεταγµένες της στο πλέγµα.
int MPI_Cart_rank (
MPI_Comm
comm.,
int*
cords,
int*
rank)
όπου
comm είναι ο communicator του πλέγµατος
coords είναι ένας πίνακας µε τις συντεταγµένες της διεργασίας
rank είναι ένας δείκτης στο σηµείο που θα αποθηκευτεί ο αριθµός της διεργασίας.
Κατά τη φάση όπου µια γραµµή του πίνακα αποστέλλεται στην πρώτη διεργασία της
αντίστοιχης γραµµής του πλέγµατος των διεργασιών θα έχουµε:
int
dest_coords[2];
int
dest_id;
int
grid_id;
int
i;
:
for (i=0;i<m;i++) {
dest_coords[0]=BLOCK_OWNER(I,r,m);
dest_coords[1]=0;
MPI_Cart_rank(grid_comm,dest_coords,&dest_id);
if (grid_id==0)
{
//Read matrix row
...
//Send matrix row “i” to process “dest_id”
...
}else if (grid_id==dest_id)
{
//Receive matrix row “i” from process “0”
}
}
Η συνάρτηση MPI_Cart_coords
Η συνάρτηση MPI_Cart_coords χρησιµοποιείται για να προσδιορίσει µια διεργασία τις
συντεταγµένες της στο πλέγµα των διεργασιών:
int MPI_Cart_coords (
MPI_Comm
comm.,
int
rank,
132
∆.Σ. Βλάχος και Θ.Η. Σίµος
int*
όπου
cords)
comm είναι ο communicator του πλέγµατος
rank είναι ο αριθµός της διεργασίας
coords είναι ένας πίνακας στον οποίο θα αποθηκευτούν οι συντεταγµένες της
διεργασίας στο πλέγµα.
Η συνάρτηση MPI_Comm_split
Η συνάρτηση MPI_Comm_split χρησιµοποιείται για να οµαδοποιήσει κάποιες διεργασίες
ενός communicator σε ένα νέο communicator.
int MPI_Comm_split
(
MPI_Comm
old_comm,
int
partition,
int
new_rank,
MPI_Comm*
new_comm)
όπου
old_comm είναι ο communicator που περιέχει όλες τις διεργασίες
partition είναι ο αριθµός της υποοµάδας των διεργασιών
new_rank είναι ένας αριθµός που καθορίζει τη νέα σειρά των διεργασιών µέσα στην
υποοµάδα
new_comm είναι ένας δείκτης στο σηµείο που θα αποθηκευτεί ο νέος communicator (της
υποοµάδας)
Στην περίπτωσή µας θα χρειαστούµε να οργανώσουµε τις διεργασίες σε γραµµές. Αυτό
µπορεί να γίνει ως εξής:
MPI_Comm
int
grid_comm; //The grid communicator
grid_coords;
//Coordinates of a process in the
//grid
row_comm;
MPI_Comm
...
MPI_Comm_split(grid_comm,grid_coords[0],
Grid_coords[1],&row_comm);
Παρακάτω δίνουµε τις γενικές συναρτήσεις για ανάγνωση και εκτύπωση ενός πίνακα που
είναι διανεµηµένος σε ένα Καρτεσιανό πλέγµα:
// Function read_cheackboard_matrix.
// It reads a matrix from a file. The first two elements
133
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of the matrix
// to the MPI processes.
// The number of the processes must be a square number.
void read_checkboard_matrix
(
char*
s,
//IN - File name
void***
subs,
//OUT - 2D array
void**
storage, //OUT - Array storage area
MPI_Datatype dtype,
//IN - Element type
int*
m,
//OUT - Array rows
int*
n,
//OUT - Array cols
MPI_Comm grid_comm) //IN - Communicator
{
void*
buffer;
int
coord[2];
int
datum_size;
int
dest_id;
int
grid_coord[2];
int
grid_id;
int
grid_period[2];
int
grid_size[2];
int
i,j,k;
FILE*
infileptr;
void*
laddr;
int
local_cols;
int
local_rows;
int
p;
void*
raddr;
MPI_Status status;
MPI_Comm_rank(grid_comm,&grid_id);
MPI_Comm_size(grid_comm,&p);
datum_size=get_size(dtype);
//
these
//
if
Process 0 opens file, reads 'm' and 'n' and broadcasts
to
the other processes.
(grid_id==0)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*m=0;
else {
fread(m,sizeof(int),1,infileptr);
fread(n,sizeof(int),1,infileptr);
}
}
MPI_Bcast(m,1,MPI_INT,0,grid_comm);
if (!(*m))
134
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Abort(MPI_COMM_WORLD,OPEN_FILE_ERROR);
MPI_Bcast(n,1,MPI_INT,0,grid_comm);
);
// Each process determines the size of the submatrix
// that it is responsible for.
MPI_Cart_get(grid_comm,2,grid_size,grid_period,grid_coord
local_rows=BLOCK_SIZE(grid_coord[0],grid_size[0],*m);
local_cols=BLOCK_SIZE(grid_coord[1],grid_size[1],*n);
// Dynamically allocate 2D matrix
*storage=my_malloc(grid_id,local_rows*local_cols*datum_si
ze);
*subs=(void**)my_malloc(grid_id,local_rows*PTR_SIZE);
for(i=0;i<local_rows;i++)
);
(*subs)[i]=&(((char*)(*storage))[i*local_cols*datum_size]
// Grid process 0 reads in the matrix one row at a time
// and distributes it in the proper process
if (grid_id==0)
buffer=my_malloc(grid_id,*n*datum_size);
// For each row of processes ...
for (i=0;i<grid_size[0];i++) {
coords[0]=i;
// For each matrix row controlled by this process..
for (j=0;j<BLOCK_SIZE(i,grid_size[0],*m);j++)
{
if (grid_id==0)
fread(buffer,datum_size,*n,infileptr);
// Distribute it
for (k=0;k<grid_size[1];k++) {
coords[1]=k;
// Find address of first element to send
raddr=buffer+BLOCK_LOW(k,grid_size[1],*n)*datum_size;
// Determine the grid id of the process getting
the subrow
MPI_Cart_rank(grid_comm,coords,&dest_id);
//Send..
if (grid_id==0)
{
if (dest_id==0)
{
laddr=(*subs)[j];
memcpy(laddr,raddr,local_rows*datum_size);
}else{
MPI_Send(raddr,BLOCK_SIZE(k,grid_size[1],*n),dtype,dest_i
d,0,grid_comm);
}
135
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
}else if (grid_id==dest_id)
{
MPI_Recv(((*subs)[j],local_cols,dtype,0,0,grid_comm,&stat
us);
}
}
}
}
if (grid_id==0)
free(buffer);
}
// Function print_checkboard_matrix
// Prints the contents of a matrix that is
// distributed checkboard fashion among the processes
void print_checkboard_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
grid_comm) //IN - Communicator
{
void*
buffer;
int
coords[2];
int
datum_size;
int
els;
int
grid_coords[2];
int
grid_id;
int
grid_period[2];
int
grid_size[2];
int
i,j,k;
void*
laddr;
int
local_cols;
int
p;
int
src;
MPI_Status status;
MPI_Comm_rank(grid_comm,&grid_id);
MPI_Comm_size(grid_comm,&p);
datum_size=get_size(dtype);
MPI_Cart_get(grid_comm,2,grid_size,grid_period,grid_coord
s);
local_cols=BLOCK_SIZE(grid_coords[1],grid_size[1],n);
if (!grid_id)
buffer=my_malloc(grid_id,n*datum_size);
// For each row in the process grid..
for (i=0;i<grid_size[0],i++) {
coords[0]=i;
136
∆.Σ. Βλάχος και Θ.Η. Σίµος
// For each matrix row in the current process
for (j=0;j<BLOCK_SIZE(i,grid_size[0],m);j++) {
// Collect the matrix row int the process 0
// and then print it
if (!grid_id) {
for (k=0;k<grid_size[1];k++) {
coords[1]=k;
MPI_Cart_rank(grid_comm,coords,&src);
els=BLOCK_SIZE(k,grid_size[1],n);
laddr=buffer+BLOCK_LOW(k,grid_size[1],n)*datum_size;
if (src==0) {
memcpy(laddr,a[j],els*datum_size);
}else {
}
MPI_Recv(laddr,els,dtype,src,0,grid_comm,&status);
}
}
print_subvector(buffer,dtype,n);
putchar('\n');
}else if (grid_coords[0]==i) {
MPI_Send(a[j],local_cols,dtype,0,0,grid_comm);
}
}
}
if (!grid_id) {
free(buffer);
putchar('\n');
}
137
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ανάλυση
Στο παρακάτω σχήµα φαίνεται η καµπύλη επιτάχυνσης για τις τρεις µεθόδους που
αναπτύξαµε:
Σχήµα 15. Καµπύλη επιτάχυνσης για τις τρεις µεθόδους που αναπτύχθηκαν. τεµαχισµός
κατά γραµµές, „ τεµαχισµός κατά στήλες και • τεµαχισµός σε Καρτεσιανό πλέγµα.
138
∆.Σ. Βλάχος και Θ.Η. Σίµος
Πολλαπλασιασµός
πινάκων
Εισαγωγή
Ενώ ο πολλαπλασιασµός πίνακα µε διάνυσµα είναι µια από τις πιο συνηθισµένες
λειτουργίες που γίνονται στον υπολογιστή για τη λύση ενός επιστηµονικού προβλήµατος,
ο πολλαπλασιασµός πίνακα µε πίνακα (και ειδικά µεγάλων πινάκων που θα ήταν
επιβεβληµένη η χρήση παράλληλων υπολογιστών) συναντάται µόνο σε κάποιες
εφαρµογές της υπολογιστικής φυσικής και της επεξεργασίας σήµατος.
Στο κεφάλαιο αυτό θα παρουσιαστούν δύο σειριακές τεχνικές για τον πολλαπλασιασµό
πινάκων και στη συνέχεια θα αναζητηθούν τρόποι για τον παραλληλισµό του
προβλήµατος.
Σειριακός πολλαπλασιασµός πινάκων
Επαναληπτικός κατά γραµµές αλγόριθµος
Το γινόµενο του πίνακα Αlm µε τον πίνακα Bmn είναι ο πίνακας Cln, του οποίου το
στοιχείο cij δίνεται:
m −1
cij = ∑ aik bkj
k =0
Ένας σειριακός αλγόριθµος για τη λύση του προβλήµατος είναι:
Είσοδος:
Έξοδος:
a[0..l-1,0..m-1]
b[0..m-1,0..n-1]
c[0..l-1,0..n-1]
for i=0 to l-1
for j=0 to n-1
c[i,j]=0
for k=0 to m-1
c[i,j]=c[i,j]+a[i,k]*b[k,j]
endfor
endfor
endfor
Ο αλγόριθµος απαιτεί l*m*n προσθέσεις και τον ίδιο αριθµό πολλαπλασιασµών. Έτσι, η
πολυπλοκότητα του αλγόριθµου είναι Θ(n3) (ας υποθέσουµε για ευκολία ότι l=m=n). Η
υλοποίηση του παραπάνω αλγόριθµου σε C είναι µια εύκολη υπόθεση. Υπάρχει όµως ένα
πρόβληµα µε αυτόν τον αλγόριθµο.
139
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ας υποθέσουµε ότι οι πίνακες αποθηκεύονται στη µνήµη κατά γραµµές (όπως συµβαίνει
στη C). Αν ο πίνακας Β είναι αρκετά µικρός, τότε υπάρχει µεγάλη πιθανότητα όλος ο
πίνακας ή ένα µεγάλο µέρος του να βρίσκεται στη γρήγορη µνήµη του υπολογιστή. Αν ο
πίνακας όµως είναι µεγάλος, τότε στον εσωτερικό βρόγχο του αλγόριθµου όπου
χρειάζονται τα στοιχεία µιας στήλης του Β (δηλαδή από διαφορετικές γραµµές), υπάρχει
ελάχιστη πιθανότητα αυτά τα στοιχεία να βρίσκονται στη γρήγορη µνήµη, και γι’ αυτό το
λόγο η απόδοση του αλγόριθµου θα µειωθεί δραµατικά.
Σχήµα 1. Απόδοση του επαναληπτικού αλγόριθµου κατά γραµµές σε σχέση µε το µέγεθος
του πίνακα Β.
Σχήµα 2. Σε µια µόνο επανάληψη του επαναληπτικού κατά γραµµές αλγόριθµου διαβάζεται
όλος ο πίνακας Β για την παραγωγή µιας γραµµής του πίνακα C.
Αναδροµικός κατά τµήµατα αλγόριθµος
Για να πολλαπλασιαστούν οι πίνακες Α και Β θα πρέπει ο αριθµός των στηλών του Α να
είναι ίδιος µε τον αριθµό των γραµµών του Β. Ας υποθέσουµε ότι ο Α έχει l γραµµές και
m στήλες και ο πίνακας Β έχει m γραµµές και n στήλες. Μπορούµε να διαιρέσουµε τον
πίνακα Α σε µικρότερους υποπίνακες
140
∆.Σ. Βλάχος και Θ.Η. Σίµος
A
A =  00
 A10
A01 

A11 
και το ίδιο κάνουµε για τον πίνακα Β:
B
B =  00
 B10
B01 

B11 
έτσι ώστε ο αριθµός των στηλών στους υποπίνακες Α00 και Α10 να είναι ίδιος µε των
αριθµό των γραµµών στους υποπίνακες Β00 και Β01. Το γινόµενο τώρα C=AB µπορεί να
γραφεί:
 A B + A01 B10
C =  00 00
 A10 B00 + A11 B10
A00 B01 + A01 B11 

A10 B01 + A11 B11 
όπου κάθε ένα από τα γινόµενα AikBkj είναι ένα γινόµενο πινάκων.
Αν ο πίνακας Β είναι πολύ µεγάλος και δε χωρά στη γρήγορη µνήµη του υπολογιστή,
µπορούµε να χωρίσουµε τους πίνακες Α και Β σε τέσσερις υποπίνακες και να
υπολογίσουµε τα νέα γινόµενα. Αν και πάλι, οι υποπίνακες δεν χωρούν στη γρήγορη
µνήµη, µπορούµε να ξανακάνουµε τη διαίρεση σε υποπίνακες για κάθε έναν από τους
υποπίνακες του προηγούµενου βήµατος. Αυτή η διαδικασία µπορεί να γίνει µε
αναδροµικό τρόπο.
Σχήµα 3. Αναδροµικός πολλαπλασιασµός πινάκων.
Παρακάτω δίνουµε τον κώδικα για τον αναδροµικά πολλαπλασιασµό των δύο πινάκων:
double
a[N][N], b[N][N], c[N][N];
void mm(
int
crow, int
int
arow, int
int
brow, int
ccol, //Corner of c
acol, //Corner of a
bcol, //Corner of b
141
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
{
}
int
int
int
l,
m,
n)
int
int
int
int
double*
double*
double*
//Block a is lxm
//Block b is mxn
//Block c is lxn
lhalf[3];
mhalf[3];
nhalf[3];
i,j,k;
aptr;
bptr;
cptr;
if (m*n>THRESHOLD)
{
// Matrix b does not fit in cache memory
lhalf[0]=0;
lhalf[1]=l/2;
lhalf[2]=l-l/2;
mhalf[0]=0;
mhalf[1]=m/2;
mhalf[2]=m-m/2;
nhalf[0]=0;
nhalf[1]=n/2;
nhalf[2]=n-n/2;
for (i=0;i<2;i++)
for (j=0;j<2;j++)
for (k=0;k<2;k++)
mm(
crow+lhalf[i],ccol+mhalf[j],
arow+lhalf[i],acol+mhalf[k],
brow+mhalf[k],bcol+nhalf[j],
lhalf[i+1],mhalf[k+1],nhalf[j+1]);
}else {
// Matrix b fits in cache memory
for (i=0;i<l;i++)
for (j=0;j<n;j++) {
cptr=&c[row+i][ccol+i];
aptr=&a[arow+i][acol];
bptr=&b[brow][bcol+j];
for (k=0;k<m;k++) {
*cptr+=*(aptr++)*(*bptr);
bprt+=N;
}
}
}
Αρχικά η κλήση της συνάρτησης γίνεται mm(0,0,0,0,0,0,N,N,N).
142
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 4. Σύγκριση της απόδοσης του αναδροµικού και επαναληπτικού αλγόριθµου για τον
παλλαπλασιασµό πινάκων.
Κατά γραµµές παράλληλος αλγόριθµος
Ας δούµε λίγο ποιες είναι οι αρχέγονες διεργασίες για τον υπολογισµό του γινοµένου
C=AB. Κάθε στοιχείο cij του πίνακα C είναι µια συνάρτηση των στοιχείων του Α και Β
και µπορεί να υπολογιστεί παράλληλα µε τα υπόλοιπα στοιχεία του πίνακα C. Έτσι,
µπορούµε να θεωρήσουµε ότι το στοιχείο cij είναι η αρχέγονη διεργασία και απαιτεί τη
γραµµή i του πίνακα Α και τη στήλη j του πίνακα Β. Αυτή η παρατήρηση µας καθοδηγεί
και στο πως θα συσσωρεύσουµε τις αρχέγονες διεργασίες σε µεγαλύτερες διεργασίες.
Έτσι τα στοιχεία της ίδιας γραµµής του πίνακα C θα απαιτούν µια µόνο γραµµή του
πίνακα Α και τα στοιχεία της ίδιας στήλης του πίνακα C θα απαιτούν µια µόνο στήλη του
πίνακα B. Και οι δύο συσσωρεύσεις είναι όµοιες. Θα επιλέξουµε παρακάτω την πρώτη
από τις δύο.
Είναι πιο απλό να χρησιµοποιήσουµε την ίδια συσσώρευση για όλους τους πίνακες. Με
αυτόν τον τρόπο µπορούµε να χρησιµοποιήσουµε το αποτέλεσµα ενός πολλαπλασιασµού
σαν παράγοντα για την υλοποίηση ενός άλλου.
Ας δούµε τι µπορεί να κάνει η διεργασία i µε τη γραµµή i του πίνακα Α, τη γραµµή i του
πίνακα Β και τη γραµµή ι του πίνακα C. Έχουµε ότι:
m −1
cij = ∑ aik bkj
k =0
Έτσι, η διεργασία i µπορεί να υπολογίσει το στοιχείο aiibi0 που είναι ένας παράγοντας για
το στοιχείο ci0. Επίσης, µπορεί να υπολογίσει το στοιχείο aiibi1 που είναι ένας παράγοντας
για το στοιχείο ci1. Έτσι, η διεργασία i µπορεί να έναν παράγοντα για κάθε στοιχείο της
143
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
γραµµής i του πίνακα C. Αυτό που µένει τώρα είναι να παραχθούν όλοι οι παράγοντες,
πράγµα που σηµαίνει ότι η διεργασία i πρέπει να προσπελάσει όλες τις γραµµές του
πίνακα Β.
Μπορούµε τώρα να οργανώσουµε τις διεργασίες σε έναν δακτύλιο και κάθε διεργασία να
περνά τη γραµµή του πίνακα Β που διαθέτει στην επόµενή της διεργασία. Έτσι, µετά από
m βήµατα όλες οι διεργασίες θα έχουν προσπελάσει όλες τις γραµµές του πίνακα Β και
όλοι οι παράγοντες θα έχουν υπολογιστεί. Επειδή ο αριθµός των γραµµών του πίνακα Β
θα είναι πολύ µεγαλύτερος από τον αριθµό των επεξεργαστών συσσωρεύουµε τµήµατα
συνεχόµενων γραµµών σε µεγαλύτερες διεργασίες.
144
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 5. Σχηµατική αναπαράσταση των επικοινωνιών για την υλοποίηση του παράλληλου
αλγόριθµου πολλαπλασιασµού πινάκων µε τεµαχισµό κατά γραµµές.
145
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Τέλος, ας δούµε τις δυνατότητες κλιµάκωσης του αλγόριθµου που αναπτύξαµε. Ας
υποθέσουµε για ευκολία ότι όλοι οι πίνακες έχουν n γραµµές και n στήλες και το n είναι
πολλαπλάσιο του p. Κάθε διεργασία έχει n/p γραµµές του πίνακα B που τις µεταδίδει
στην επόµενη διεργασία σε κάθε βήµα του αλγόριθµου.
Όταν ξεκινά ο αλγόριθµος, κάθε διεργασία αρχικοποιεί το n/p x n τµήµα του πίνακα C
που διαθέτει στο 0. Σε κάθε επανάληψη, πολλαπλασιάζει ένα τµήµα n/p x n/p του
τµήµατος του πίνακα Α που διαθέτει µε το τµήµα n/p x n του πίνακα Β. Έπειτα προσθέτει
τον n/p x n πίνακα που παράγει στο τµήµα του C που διαθέτει. Αν χ είναι ο χρόνος για
την πρόσθεση και τον πολλαπλασιασµό στον εσωτερικό βρόγχο, ο συνολικός χρόνος σε
κάθε επανάληψη θα είναι:
χ (n / p)(n / p )n = χ n3 / p 2
Τέλος, σε κάθε βήµα, κάθε διεργασία θα στείλει το τµήµα του Β που διαθέτει στην
επόµενη και αυτό θα γίνει ταυτόχρονα για όλες τις διεργασίες. Ο χρόνος που απαιτείται
γι’ αυτό είναι:
λ + (n / p )n / β
Ο αλγόριθµος έχει p επαναλήψεις και έτσι ο συνολικός χρόνος θα είναι:
p ( χ n3 / p 2 + λ + n 2 /( pβ )) = χ n3 / p + pλ + n 2 / β
Ο σειριακός αλγόριθµος έχει πολυπλοκότητα Θ(n3). Η επικοινωνιακή πολυπλοκότητα
είναι Θ(pn2). Έτσι η µετρική της ισο-απόδοσης θα είναι:
n3 ≥ Cpn 2 ⇒ n ≥ Cp
Επειδή Μ(n)=n2 η συνάρτηση κλιµάκωσης θα είναι:
M (Cp ) / p = C 2 p 2 / p = C 2 p
πράγµα που σηµαίνει ότι ο αλγόριθµος δεν έχει πολύ καλές ιδιότητες κλιµάκωσης.
Ο αλγόριθµος του Cannon
Στον προηγούµενο αλγόριθµο που αναπτύξαµε, µια διεργασία µε n/p γραµµές του Α,
πολλαπλασιάζει έναν n/p x n/p υποπίνακα του Α µε έναν n/p x n υποπίνακα του Β και
στη συνέχεια µεταδίδει n/p*n=n2/p στοιχεία. Ο λόγος υπολογισµών προς στοιχεία που
µεταδίδονται θα είναι:
2n 3 / p 2n
=
n2 / p
p
Ας προσπαθήσουµε να αυξήσουµε αυτόν το λόγο µε σκοπό να αυξήσουµε την απόδοση
του αλγόριθµου. Μια διεργασία που έχει τη i γραµµή του πίνακα Α είναι υπεύθυνη για
146
∆.Σ. Βλάχος και Θ.Η. Σίµος
την παραγωγή όλης της i γραµµής του πίνακα C και για το λόγο αυτό χρειάζεται να
έχουµε πρόσβαση σε όλα το στοιχεία του πίνακα Β (βλ. σχήµα 6α). Σε αντίθεση, αν
συσσωρεύσουµε στις διεργασίες ένα µόνο τµήµα του πίνακα C τότε ο αριθµός των
στοιχείων του πίνακα Β που είναι απαραίτητα θα µειωθεί µε αποτέλεσµα να ελαττωθεί
δραµατικά και ο όγκος των στοιχείων που θα µεταδοθούν (βλ σχήµα 6b).
Σχήµα 6. Όταν µια διεργασία είναι υπεύθυνη για την παραγωγή µιας ολόκληρης γραµµής
του πίνακα C, τότε απαιτεί όλον τον πίνακα Β (a) ενώ αν είναι υπεύθυνη µόνο για ένα
τµήµα του C τότε απαιτεί πολύ λιγότερα στοιχεία από τον Β (b).
Για απλότητα θα υποθέσουµε ότι όλοι οι πίνακες είναι τετραγωνικοί διαστάσεων nxn, το
p είναι τετράγωνος αριθµός και το n είναι πολλαπλάσιο του √p. Κάθε διεργασία είναι
υπεύθυνη για τον υπολογισµό ενός (n/√p) x (n/√p) τµήµατος του πίνακα C. Για τον
υπολογισµό, κάθε διεργασία χρειάζεται n/√p γραµµές του πίνακα Α και n/√p στήλες του
πίνακα Β.
Πάλι οι υπολογισµοί ανά διεργασία είναι 2n3/p. Ο αριθµός των στοιχείων που κάθε
διεργασία θα χρειαστεί είναι 2n(n/√p). Ο λόγος τώρα των υπολογισµών ανά στοιχεία που
µεταδίδονται γίνεται:
2n 3 / p
n
=
2
2n / p
p
και για p>4 ισχύει:
147
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
n
2n
>
p
p
Άρα ο παραπάνω αλγόριθµος, που είναι γνωστός και σαν αλγόριθµος του Cannon, όταν
ο αριθµός των επεξεργαστών είναι µεγαλύτερος του 4 θα έχει καλύτερη απόδοση από τον
αλγόριθµο που αναπτύξαµε στην προηγούµενη παράγραφο.
Ας δούµε λίγο τώρα πως θα υλοποιηθούν οι επικοινωνίες µεταξύ των διεργασιών, µε
δεδοµένο ότι αρχικά κάθε διεργασία θα αντιστοιχεί σε έναν κόµβο ενός διδιάστατου
πλέγµατος και θα έχει ένα τετραγωνικό κοµµάτι των πινάκων Α και Β.
Αρχικά λοιπόν η διεργασία Pij θα έχει το τµήµα Αij του Α και το τµήµα Βij του Β. Σκοπός
µας είναι τελικά η διεργασία Pij να αποκτήσει το τµήµα Cij του πίνακα C. Επίσης
σηµειώνουµε εδώ ότι ο υποπίνακας Αij περιέχει τις γραµµές li,li+1,…,hi και τις στήλες
lj,lj+1,…,hj. Τότε το στοιχείο cmn του πίνακα C µπορεί να γραφεί:
n −1
cmn = ∑ amk bkn ⇒
k =0
cmn =
p −1 hi
∑ ∑a
i = 0 r =li
b
mr rn
και αν υποθέσουµε ότι η m γραµµή ανήκει στη διαµέριση (m) και η n στήλη στη
διαµέριση (n):
cmn =
p −1
∑A
i =0
( m ),i
Bi ,( n )
Εποµένως, η διεργασία P(m),(n) που είναι υπεύθυνη για τον υπολογισµό του υποπίνακα
C(m),(n) θα υπολογίσει το άθροισµα:
A( m ),0 B0,( n ) + A( m ),1 B1,( n ) + ...
Στο σχήµα 7α φαίνεται η αρχική κατανοµή των υποπινάκων στις διεργασίες. Αν τώρα
ολισθήσουµε κυκλικά προς τα αριστερά σε κάθε γραµµή i τους υποπίνακες του Α κατά i
θέσεις και το ίδιο κάνουµε προς τα πάνω µε τους υποπίνακες του Β (δηλαδή η j στήλη
ολισθαίνει προς τα πάνω τους υποπίνακες του Β κατά j θέσεις) τότε θα προκύψει η
κατανοµή των υποπινάκων που φαίνεται στο σχήµα 7b µε τα περιεχόµενα των
διεργασιών να είναι έτοιµα για τον πρώτο πολλαπλασιασµό.
148
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 7. Αρχικοποίηση του περιεχοµένου των διεργασιών. Αρχική κατανοµή (α) και τελική
κατανοµή (b) που προκύπτει αν ολισθήσουµε τους υποπίνακες του Α στην i γραµµή i θέσεις
αριστερά και ολισθήσουµε τους υποπίνακες του Β στην j στήλη κατά j θέσεις προς τα πάνω.
Ο υπολογισµός τώρα των υποπινάκων C(m),(n) είναι προφανής και αποτυπώνεται στο
παρακάτω σχήµα:
149
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 8. Αναπαράσταση των λειτουργιών που εκτελεί η διεργασία P1,2 στον αλγόριθµο του
Cannon. Κάθε φορά που γίνεται ένας πολλαπλασιασµός, η γραµµή 1 ολισθαίνει µία θέση
αριστερά και η στήλη 2 ολισθαίνει µία θέση προς τα πάνω. Μετά από √16=4 βήµατα θα
έχουν υπολογιστεί όλοι οι παράγοντες που χρειάζονται για τον υπολογισµό του C1,2.
Ας δούµε τώρα την πολυπλοκότητα του αλγόριθµου. Σε κάθε βήµα θα γίνουν
3
χ ( n / p )3 = χ n
p
3
2
πράξεις, και αυτό θα γίνει √p φορές. Ο συνολικός υπολογιστικός χρόνος δηλαδή θα είναι:
xn
3
p
Όσον αφορά τώρα στις επικοινωνίες, πριν αρχίσει ο αλγόριθµος, θα πρέπει να
τακτοποιηθούν τα περιεχόµενα των υποπινάκων στις διεργασίες και αυτό απαιτεί χρόνο
150
∆.Σ. Βλάχος και Θ.Η. Σίµος
2(λ +
n2
)
pβ
όπου έχουµε θεωρήσει ότι οι επικοινωνίες γίνονται ταυτόχρονα.
Σε κάθε µια από τις √p επαναλήψεις µεταδίδονται και λαµβάνονται δύο υποπίνακες, άρα
συνολικά θα έχουµε χρόνο
n2
)
p 2(λ +
pβ
Έτσι, ο συνολικός χρόνος θα είναι:
n3
n2
χ + 2( p + 1)(λ +
)
p
pβ
Ο σειριακός αλγόριθµος έχει πολυπλοκότητα Θ(n3). Η επικοινωνιακή πολυπλοκότητα
είναι pΘ(n2/√p), δηλαδή Θ(n2√p). Η µετρική της ισο-απόδοσης είναι:
n3 ≥ C pn 2 ⇒ n ≥ C p
Επειδή Μ(n)=n2 η συνάρτηση κλιµάκωσης είναι:
M (C p ) / p = C 2 p / p = C 2
πράγµα που σηµαίνει ότι έχουµε σταθερή µνήµη ανά επεξεργαστή και άρα ο αλγόριθµος
του Cannon έχει ιδανική συµπεριφορά όσον αφορά στην κλιµάκωση.
151
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
152
∆.Σ. Βλάχος και Θ.Η. Σίµος
Επίλυση γραµµικών
συστηµάτων
Εισαγωγή
Η επίλυση γραµµικών συστηµάτων είναι µια πολύ συνηθισµένη διαδικασία στην
αντιµετώπιση επιστηµονικών προβληµάτων. Πολλές φορές, διάφορα προβλήµατα,
γραµµικά και µη γραµµικά, µοντελοποιούνται µε τέτοιο τρόπο, ώστε η λύση τους να
προκύπτει από ένα σύστηµα γραµµικών εξισώσεων.
Υπάρχουν δύο τρόποι για να λύσει κανείς ένα τέτοιο σύστηµα. Ο ένας είναι
εφαρµόζοντας τη µέθοδο των απαλοιφών, όπου διαδοχικά απαλείφονται οι άγνωστοι από
τις εξισώσεις, και ο άλλος είναι µε τη χρήση επαναληπτικών τεχνικών που προσεγγίζουν
τη λύση.
Ορολογία
Μια γραµµική εξίσωση µε n µεταβλητές x0, x1, …, xn-1 είναι της µορφής:
a0 x0 + a1 x1 + ... + an −1 xn −1 = b
όπου τα a0,a1,…, an-1 και b είναι σταθερές.
Ένα πεπερασµένο σύνολο από γραµµικές εξισώσεις λέγεται και σύστηµα γραµµικών
εξισώσεων. Ένα σύνολο από αριθµούς s0, s1, …, sn-1 λέγεται λύση του συστήµατος αν
αντικαθιστώντας x0=s0, x1=s1, …, xn-1=sn-1 τότε ικανοποιούνται όλες οι εξισώσεις του
συστήµατος.
Ένα σύστηµα n εξισώσεων µε n αγνώστους έχει τη µορφή:
a0,0 x0 + a0,1 x1 + ... + a0, n −1 xn −1 = b0
a1,0 x0 + a1,1 x1 + ... + a1, n −1 xn −1 = b1
...
an −1,0 x0 + an −1,1 x1 + ... + an −1, n −1 xn −1 = bn −1
και µπορεί να γραφεί και
Ax = b
όπου Α είναι ένας nxn πίνακας και τα x και b είναι nx1 διανύσµατα.
153
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ένας nxn πίνακας Α λέγεται συµµετρικά φραγµένος µε εύρος w αν
i − j > w ⇒ ai , j = 0
Ένας nxn πίνακας Α λέγεται άνω τριγωνικός αν
i > j ⇒ ai , j = 0
και κάτω τριγωνικός αν
i < j ⇒ ai , j = 0
Ένας nxn πίνακας A λέγεται σχεδόν διαγώνιος αν
ai ,i ≥ ∑ ai , j , 0 ≤ i < n
j ≠i
Ένας nxn πίνακας Α λέγεται συµµετρικός αν
ai , j = a j ,i
Ένας nxn πίνακας Α λέγεται θετικά ορισµένος αν
∀x ≠ 0, xT Ax > 0
όπου x είναι ένα nx1 διάνυσµα και το xT το ανάστροφο 1xn διάνυσµα του x.
Λύση άνω τριγωνικού συστήµατος
Ας υποθέσουµε ότι ο πίνακας Α είναι άνω τριγωνικός. Ένα παράδειγµα ενός τέτοιου
συστήµατος φαίνεται παρακάτω:
1x0 + 1x1 − 1x2 + 4 x3
−2 x1 − 3 x2 + 1x3
2 x2 − 3x3
2 x3
=8
=5
=0
=4
Η τελευταία εξίσωση µπορεί να λυθεί αφού έχει µόνο έναν άγνωστο. Αντικαθιστώντας
στις προηγούµενες προκύπτει:
154
∆.Σ. Βλάχος και Θ.Η. Σίµος
1x0 + 1x1 − 1x2
−2 x1 − 3x2
2 x2
=0
=3
=6
2 x3 = 4
Τώρα η τρίτη εξίσωση µπορεί να λυθεί. Αντικαθιστώντας στις δύο προηγούµενες
προκύπτει:
1x0 + 1x1
−2 x1
=3
= 12
2 x2
=6
2 x3 = 4
και τελικά αντικαθιστώντας στην πρώτη εξίσωση την τιµή του x1 προκύπτει:
1x0
−2 x1
=9
= 12
2 x2
=6
2 x3 = 4
Ο ψευδοκώδικας φαίνεται παρακάτω:
a[0..n-1,0..n-1]-οι συντελεστές του συστήµατος
b[0..n-1]-οι σταθερές
x[0..n-1]-η λύση
for i=n-1 downto 1 do
x[i]=b[i]/a[i,i]
for j=0 to i-1 do
b[j]=b[j]-x[i]xa[j,i]
a[j,i]=0
endfor
endfor
Η πολυπλοκότητα του αλγόριθµου είναι Θ(n2). Στο παρακάτω σχήµα 1 φαίνεται ο γράφος
εξάρτησης δεδοµένων για τον παραπάνω αλγόριθµο. Όπως φαίνεται σε αυτό το σχήµα,
δεν είναι δυνατόν να εκτελέσουµε παράλληλα τον εξωτερικό βρόγχο του αλγόριθµου,
αλλά µπορούµε να παραλληλοποιήσουµε τον εσωτερικό βρόγχο.
155
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 1. Γράφος εξάρτησης δεδοµένων για τη λύση άνω τριγωνικού συστήµατος
γραµµικών εξισώσεων µε διαδοχικές αντικαταστάσεις.
Παραλληλοποίηση κατά γραµµές
Ας υποθέσουµε ότι αντιστοιχίζουµε σε κάθε γραµµή του πίνακα Α µια αρχέγονη
διεργασία (µαζί µε τις αντίστοιχες τιµές του x και του b). Κατά τη διάρκεια της iεπανάληψης, η διεργασία που σχετίζεται µε τη γραµµή j, πρέπει να υπολογίσει τη νέα
τιµή του bj, πράγµα που σηµαίνει πως πρέπει να γνωρίζει την τιµή του xi και του aj,i. Και
ενώ γνωρίζει (γιατί διαθέτει) την τιµή aj,i δε συµβαίνει το ίδιο µε το xi. Για το λόγο αυτό,
η διεργασία i πρέπει αφού υπολογίσει το xi να το µεταδώσει σε όλες τις προηγούµενες
διεργασίες.
Για οµοιόµορφη κατανοµή του υπολογιστικού φόρτου, συσσωρεύουµε τις αρχέγονες
διεργασίες µε την τεχνική της διαφύλλωσης: η διεργασία k ελέγχει τη γραµµή i αν και
µόνο αν i mod p=k (βλ. σχήµα 2α).
Σχήµα 2. Η τεχνική της διαφύλλωσης στην περίπτωση της συσσώρευσης κατά γραµµές (α)
και κατά στήλες (b).
156
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ο εσωτερικός βρόγχος του αλγόριθµου θα εκτελεστεί περίπου n/2p φορές. Ο εξωτερικός
βρόγχος εκτελείται N-1 φορές και έτσι η συνολική υπολογιστική πολυπλοκότητα είναι
Θ(n2/p).
Επειδή θα γίνουν n-1 µεταδώσεις όπου κάθε φορά µεταδίδεται µόνο ένα στοιχείο, ο
συνολικός επικοινωνιακός χρόνος είναι Θ(nlogp) και η επικοινωνιακή πολυπλοκότητα θα
είναι Θ(nplogp). Έτσι, η µετρική της ισοαπόδοσης θα είναι:
n 2 ≥ Cnp log p ⇒ n ≥ Cp log p
Η συνάρτηση κλιµάκωσης θα είναι (Μ(n)=n2):
M (Cp log p) / p = C 2 p 2 log 2 p / p = C 2 p log 2 p
Παραλληλοποίηση κατά στήλες
Μια εναλλακτική σχεδίαση είναι αυτή που αντιστοιχεί σε κάθε αρχέγονη διεργασία µια
στήλη του πίνακα Α. Έτσι, η διεργασία j είναι υπεύθυνη για τη στήλη j του Α και τον
άγνωστο xj. Επίσης, στην αρχή του αλγόριθµου, η διεργασία n-1 είναι υπεύθυνη και για
το διάνυσµα b.
Συσσωρεύουµε πάλι τις διεργασίες µε την τεχνική της διαφύλλωσης, δηλαδή η στήλη i
ανήκει στη διεργασία k αν I mod p=k (βλ. σχήµα 2b).
Κατά τη διάρκεια της πρώτης επανάληψης, η διεργασία (n-1) mod p έχει το στοιχείο xn-1
και το διάνυσµα b και έτσι µπορεί να ενηµερώσει το διάνυσµα b χωρίς επικοινωνία. Στο
επόµενο βήµα, η διεργασία (n-2) mod p έχει το xn-2 αλλά πρέπει να λάβει το διάνυσµα b
από τη διεργασία (n-1) mod p. Έτσι, σε κάθε βήµα του αλγόριθµου, κάθε διεργασία αφού
υπολογίσει το b, το µεταδίδει στην προηγούµενη διεργασία. Ο αλγόριθµος θα έχει
πολυπλοκότητα Θ(n2) όπως και ο σειριακός.
Σε κάθε επανάληψη µεταδίδονται κατά µέσο όρο n/2 στοιχεία. Έτσι, ο επικοινωνιακός
φόρτος θα έχει πολυπλοκότητα χρόνου Θ(n) και αφού υπάρχουν n επαναλήψεις ο
συνολικός επικοινωνιακός χρόνος θα είναι Θ(n2).
Παρ’ όλο που η παραλληλοποίηση κατά γραµµές αναµένεται να δώσει µεγαλύτερη
επιτάχυνση από την παραλληλοποίηση κατά στήλες, επειδή στην παραλληλοποίηση κατά
στήλες έχουµε µικρότερο επικοινωνιακό φόρτο, για σταθερό n και αυθαίρετα µεγάλο p, ο
αλγόριθµος κατά στήλες θα αποδειχθεί καλύτερος. Το φαινόµενο αυτό φαίνεται στο
παρακάτω σχήµα.
157
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 3. Ανάλογα µε την τιµή του n και του p, η παραλληλοποίηση κατά στήλες µπορεί να
είναι καλύτερη από αυτήν κατά γραµµές.
Η µέθοδος των απαλοιφών του Gauss
Σειριακός αλγόριθµος
Ας δούµε τώρα τη γενική περίπτωση της επίλυσης ενός γραµµικού συστήµατος όπου ο
πίνακας Α είναι ένας τυχαίος πίνακας. Μπορούµε να κάνουµε κάποιους
µετασχηµατισµούς σε ένα σύστηµα εξισώσεων που αφήνουν αναλλοίωτη τη λύση του.
Πιο συγκεκριµένα µπορούµε:
•
να πολλαπλασιάσουµε µια εξίσωση µε µια σταθερά
•
να αναδιατάξουµε τη σειρά των εξισώσεων
•
να προσθέσουµε σε µια εξίσωση το πολλαπλάσιο µιας άλλης εξίσωσης
Η µέθοδος των απαλοιφών του Gauss έχει σαν σκοπό να φέρει τον πίνακα Α σε µια άνω
τριγωνική µορφή εφαρµόζοντας τους παραπάνω µετασχηµατισµούς. Ας δούµε πως
γίνεται αυτό µε ένα παράδειγµα. Θεωρήστε το σύστηµα:
4 x0 + 6 x1 + 2 x2 − 2 x3
2 x0
+ 5 x2 − 2 x3
−4 x0 − 3x1 − 5 x2 + 4 x3
8 x0 + 18 x1 − 2 x2 + 3 x3
=8
=4
=1
= 40
Πολλαπλασιάζουµε την πρώτη εξίσωση µε το συντελεστή –ai,0/a0,0 και την προσθέτουµε
στην εξίσωση i (i>0). Τότε προκύπτει:
158
∆.Σ. Βλάχος και Θ.Η. Σίµος
4 x0 + 6 x1 + 2 x2 − 2 x3
− 3x1 + 4 x2 − 1x3
+3x1 − 3 x2 + 2 x3
+6 x1 − 6 x2 + 7 x3
=8
=0
=9
= 24
Κάνουµε το ίδιο τώρα µε τη δεύτερη εξίσωση, δηλαδή την πολλαπλασιάζουµε µε –ai,1/a1,1
και την προσθέτουµε στην εξίσωση i (i>1). Τότε προκύπτει:
4 x0 + 6 x1 + 2 x2 − 2 x3
− 3x1 + 4 x2 − 1x3
+1x2 + 1x3
+2 x2 + 5 x3
=8
=0
=9
= 24
Τέλος κάνουµε το ίδιο και µε την τρίτη εξίσωση και προκύπτει τελικά:
4 x0 + 6 x1 + 2 x2 − 2 x3
− 3x1 + 4 x2 − 1x3
+1x2 + 1x3
3x3
=8
=0
=9
=6
Ένα λεπτό σηµείο στην εφαρµογή του παραπάνω αλγόριθµου είναι όταν στο βήµα i το
στοιχείο ai,i είναι 0 ή πολύ κοντά στο 0. Για να αντιµετωπίσουµε αυτό το πρόβληµα,
εναλλάσσουµε τη γραµµή i µε τη γραµµή j (j>i) για την οποία ισχύει:
a j ,i = max{ak ,i }
k >i
Στο σχήµα 5 φαίνεται το αποτέλεσµα αυτής της τεχνικής. Σηµειώστε εδώ, ότι τελικά ο
πίνακας Α θα είναι άνω τριγωνικός µε τη διαφορά όµως ότι τώρα οι γραµµές του πίνακα
θα είναι ανακατεµένες.
Σχήµα 4. Ένα βήµα της τεχνικής της απαλοιφής του Gauss..
159
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Παρακάτω δίνουµε τον ψευτοκώδικα για την εφαρµογή της µεθόδου των απαλοιφών του
Gauss:
for i=0 to n-1
loc[i]=i
endfor
for i=0 to n-1
// Find pivot row
magnitude=0
for j=i to n-1
if |a[loc[j],i]|>magnitude
magnitude=|a[loc[j],i]|
picked=j
endif
endfor
tmp=loc[i]
loc[i]=loc[picked]
loc[picked]=tmp
//Drive to 0 column i of unmarked rows
for j=i+1 to n-1
t=a[loc[j],i]/a[loc[i],i]
for k=i+1 to n+1
a[loc[j],k]=a[loc[j],k]-a[loc[i],k]*t
endfor
endfor
endfor
//Back substitution
for i=n-1 downto 0
x[i]=a[loc[i],n]/a[loc[i],i]
for j=0 to i-1
a[loc[j],n]=a[loc[j],n]-x[i]*a[loc[j],i]
endfor
endfor
Σηµειώστε ότι ο αλγόριθµος αντί να εναλλάσσει τις γραµµές κάνει χρήση του
διανύσµατος loc που περιέχει τη θέση της γραµµής που απαλείφει κάθε φορά.
Σχήµα 5. Με τη χρήση του διανύσµατος loc µπορούµε να φέρουµε τον πίνακα Α σε άνω
τριγωνική µορφή µε ανακατεµένες γραµµές.
160
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παράλληλος αλγόριθµος
Ας δούµε τώρα κατά πόσο µπορεί να παραλληλοποιηθεί ο αλγόριθµος των απαλοιφών
του Gauss. Ο σειριακός αλγόριθµος έχει πολυπλοκότητα Θ(n3). Μπορούµε να
διαπιστώσουµε ότι και οι δύο βρόγχοι του βασικού τµήµατος του αλγόριθµου µπορούν να
παραλληλοποιηθούν. Σε κάθε γραµµή, µόλις ο συντελεστής a[loc[j],i]/a[loc[i],i] έχει
προσδιοριστεί, οι αλλαγές στις υπόλοιπες γραµµές µπορούν να γίνουν ταυτόχρονα. Θα
εξετάσουµε δύο τεχνικές παραλληλοποίησης του αλγόριθµου.
Παραλληλοποίηση κατά γραµµές
Ας αντιστοιχίσουµε σε κάθε αρχέγονη διεργασία µια γραµµή του Α και τα αντίστοιχα
στοιχεία του b και x. Είναι εύκολο να διαπιστώσει κανείς ότι τώρα στο βήµα i του
αλγόριθµου για να επιλεγεί η γραµµή απαλοιφής θα πρέπει να γίνει µια λειτουργία
αναγωγής µεταξύ των αρχέγονων διεργασιών.
Σηµειώστε επίσης ότι στη συγκεκριµένη λειτουργία δε µας ενδιαφέρει άµεσα η τιµή του
στοιχείου που έχει την µεγαλύτερη απόλυτη τιµή αλλά ο αριθµός της γραµµής για την
οποία συµβαίνει αυτό. Έτσι, µε µια πρώτη µατιά βλέπουµε ότι θα χρειαστούµε δύο
αναγωγές: Στην πρώτη θα πρέπει να βρεθεί η µέγιστη τιµή και στη δεύτερη, αφού όλες οι
διεργασίες γνωρίζουν τώρα τη µέγιστη τιµή, κάθε διεργασία συγκρίνει τη µέγιστη τιµή µε
αυτή που διαθέτει και αν είναι ίση συµµετέχει µε τον αριθµό της αλλιώς µε το –1. Έτσι,
στο τέλος της δεύτερης αναγωγής όλες οι διεργασίες θα γνωρίζουν τον αριθµό της
διεργασίας που θα είναι υπεύθυνη για την επόµενη απαλοιφή.
Επειδή η προηγούµενη διαδικασία είναι πολύ συνηθισµένη (δηλαδή να θέλουµε να
βρούµε όχι µόνο την τιµή αλλά και τη θέση της) το MPI έχει φροντίσει αυτή η διαδικασία
να µπορεί να γίνει µε µια µόνο πράξη αναγωγής. Αυτό επιτυγχάνεται µε τη δυνατότητα
που έχει το MPI να υποστηρίζει τη σύγκριση προκαθορισµένων ζευγαριών τιµών όπως
φαίνεται στον παρακάτω πίνακα:
MPI datatype
Σηµασία
MPI_2INT
∆ύο ακέραιοι
MPI_DOUBLE_INT
Ένας double ακολουθούµενος από έναν ακέραιο
MPI_FLOAT_INT
Ένας float ακολουθούµενος από έναν ακέραιο
MPI_LONG_INT
Ένας long ακολουθούµενος από έναν ακέραιο
MPI_LONG_DOUBLE_INT
Ένας long double ακολουθούµενος από έναν ακέραιο
MPI_SHORT_INT
Ένας short ακολουθούµενος από έναν ακέραιο
Η κλήση της MPI_Allreduce θα γίνει µε το χαρακτηριστικό για τη λειτουργία
MPI_MAX_LOC. Ένα παράδειγµα κλήσης φαίνεται παρακάτω:
struct
{
double
value;
int
index;
}local,global;
...
local.value=fabs(a[j][i]);
local.index=j;
...
161
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Allreduce(&local,&global,1,MPI_DOUBLE_INT,
MPI_MAX_LOC,MPI_COMM_WORLD);
Με την εκτέλεση της προηγούµενης εντολής, όλες οι διεργασίες θα αποκτήσουν στη
µεταβλητή global τόσο την τιµή όσο και τη θέση του µεγίστου.
Ο προσδιορισµός της γραµµής απαλοιφής στο βήµα i του αλγόριθµου γίνεται σε δύο
βήµατα. Πρώτα, κάθε διεργασία βρίσκει τη γραµµή µε το µέγιστο συντελεστή από το
σύνολο των γραµµών που διαθέτει. Αυτή η λειτουργία έχει πολυπλοκότητα Θ(n/p). Στη
συνέχεια µε µια πράξη αναγωγής σαν αυτή που περιγράψαµε προσδιορίζεται η γραµµή µε
το µεγαλύτερο συντελεστή. Αυτή η διαδικασία έχει πολυπλοκότητα Θ(logp).
Υπάρχει όµως ανάγκη για µια ακόµα επικοινωνία. Κάθε διεργασία, για να υπολογίσει τη
νέα τιµή του a[j,k], χρειάζεται τις τιµές a[j,i], a[picked,i] και a[picked,k] (βλ. Σχήµα 6).
Και ενώ διαθέτει την a[j,i], τις άλλες δύο πρέπει να τις αποκτήσει µετά από µια
επικοινωνία. Η διεργασία λοιπόν που διαθέτει τη γραµµή picked µεταδίδει τις τιµές από
το i ως το n. Ο µέσος αριθµός στοιχείων που µεταδίδονται είναι n/2 και ο επικοινωνιακός
φόρτος είναι Θ(logp). Ο συνολικός χρόνος για τη µετάδοση είναι Θ(nlogp).
Σχήµα 6. Κάθε διεργασία, για να υπολογίσει τη νέα τιµή του a[j,k], χρειάζεται τις τιµές
a[j,i], a[picked,i] και a[picked,k].
Συνδυάζοντας τις δύο επικοινωνίες, ο επικοινωνιακός φόρτος θα είναι Θ(nlogp) και ο
συνολικός επικοινωνιακός χρόνος Θ(n2logp).
Ο υπολογιστικός χρόνος είναι Θ(n3/p). Η επικοινωνιακή πολυπλοκότητα είναι
Θ(n2plogp). Έτσι η µετρική της ισοαπόδοσης θα είναι:
162
∆.Σ. Βλάχος και Θ.Η. Σίµος
n 3 ≥ Cn 2 p log p ⇒ n ≥ Cp log p
Αφού M(n)=n2, η συνάρτηση κλιµάκωσης θα είναι:
M (Cp log p) / p = C 2 p 2 log 2 p / p = C 2 p log 2 p
πράγµα που σηµαίνει πως ο αλγόριθµος έχει µικρές δυνατότητες για κλιµάκωση.
Παρόµοια αποτελέσµατα προκύπτουν αν επιλέξουµε να αντιστοιχίσουµε στις αρχέγονες
διεργασίες µια στήλη του πίνακα Α.
Pipelined αλγόριθµος κατά γραµµές
Ένα βασικό χαρακτηριστικό του προηγούµενου αλγόριθµου είναι ότι κατά τη διάρκεια
των µεταδόσεων, οι διεργασίες περιµένουν χωρίς να κάνουν υπολογισµούς. Θα
µπορούσαµε να βελτιώσουµε την απόδοση ( και ιδιαίτερα τα χαρακτηριστικά
κλιµάκωσης) του αλγόριθµου αν µπορούσαµε να βρούµε έναν τρόπο να επικαλύπτονται
οι επικοινωνίες µε τους υπολογισµούς.
Ας δούµε µια διαφορετική τεχνική για την απαλοιφή. Θεωρείστε ότι στο βήµα i , η
γραµµή i βρίσκει το µεγαλύτερο στοιχείο της, έστω το k, και χρησιµοποιεί αυτό το
στοιχείο για να απαλείψει τα στοιχεία της k στήλης. Ο ψευτοκώδικας αυτής της
προσέγγισης φαίνεται παρακάτω:
for I=0 to n
loc[I]=I
endfor
for I=0 to n-1
//Find pivot column picked
magnitude=0
for j=I to n-1
if |a[i,loc[j]|>magnitude
magnitude=|a[i,loc[j]|
picked=j
endif
endfor
tmp=loc[i]
loc[i]=loc[picked]
loc[picked]=tmp
//Drive to 0 column loc[i] in rows i+1 to n-1
for j=i+1 to n-1
t=a[j,loc[i]]/a[I,loc[i]]
for k=i to n+1
a[j,loc[k]]=a[j,loc[k]]-a[i,loc[k]]*t
endfor
endfor
endfor
163
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
//Back substitution
for i=n-1 downto 0
x[loc[i]]=a[i,n]/a[i,loc[i]]
for j=0 to i-1
a[j,n]=a[j,n]-x[loc[i]]*a[j,loc[i]]
endfor
endfor
Ας δούµε τώρα πως µπορεί να παραλληλοποιηθεί ο παραπάνω αλγόριθµος. Επιλέγουµε
ένα διαφυλλωµένο τεµαχισµό κατά γραµµές του πίνακα Α και οργανώνουµε τις
διεργασίες µας σε έναν λογικό δακτύλιο.
Όταν ο αλγόριθµος ξεκινά, η διεργασία 0 ψάχνει τη γραµµή 0 για το µεγαλύτερο στοιχείο
και στέλνει αυτή τη γραµµή µαζί µε την πληροφορία για το µεγαλύτερο στοιχείο στην
επόµενη διεργασία. Ενώ διαρκεί αυτή η µετάδοση, η διεργασία 0 απαλείφει τις υπόλοιπες
γραµµές που διαθέτει.
Η διεργασία 1 περιµένει ώσπου να λάβει το µήνυµα από τη διεργασία 0. Όταν το λάβει,
αµέσως το διαβιβάζει στη διεργασία 2 και παράλληλα χρησιµοποιεί τη γραµµή που έλαβε
για να απαλείψει τις γραµµές που διαθέτει. Στο σηµείο αυτό ψάχνει τη γραµµή 1 για το
µεγαλύτερο στοιχείο, και µόλις το κάνει αυτό, στέλνει τη γραµµή 1 µαζί µε την
πληροφορία για το µεγαλύτερο στοιχείο στη διεργασία 2. Παράλληλα, απαλείφει τις
γραµµές που διαθέτει.
Η διαδικασία αυτή συνεχίζεται για κάθε διεργασία, µέχρι να φτάσει η πληροφορία µιας
διεργασίας στην προηγούµενη διεργασία. Η βήµα προς βήµα της µετάδοσης των
γραµµών έχει σαν αποτέλεσµα να επικαλύπτονται οι υπολογισµοί µε τις µεταδώσεις.
Έτσι η συνολική επικοινωνιακή πολυπλοκότητα είναι Θ(np). Η µετρική της ισοαπόδοσης
είναι:
n 3 ≥ Cnp ⇒ n ≥ Cp
Η συνάρτηση κλιµάκωσης είναι:
M ( Cp ) / p = Cp / p = C
πράγµα που σηµαίνει πως ο αλγόριθµος έχει ιδανικές ιδιότητες όσον αφορά στην
κλιµάκωση.
164
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 7. Εφαρµογή της µεθόδου των απαλοιφών του Gauss σε έναν αραιό πίνακα µε εννέα
εξισώσεις. Τα µαύρα τετράγωνα δείχνουν µη µηδενικούς συντελεστές.
165
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
166
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ταξινόµηση
Εισαγωγή
Με δεδοµένη µια ακολουθία αριθµών [α0, α1, α2, …αη-1] το πρόβληµα της ταξινόµησης
είναι να βρούµε µια διάταξη [α′0, α′1, α′2, …α′η-1] τέτοια ώστε: α′0≤α′1≤α′2…≤α′η-1. Η
ταξινόµηση είναι µια από τις πιο κοινές λειτουργίες που υλοποιούνται σε ένα σειριακό
υπολογιστή. Πολλοί αλγόριθµοι εµπλέκουν την ταξινόµηση για να µπορούν στη συνέχεια
να έχουν ευκολότερη και ταχύτερη προσπέλαση στα στοιχεία τους.
Συνήθως οι αριθµοί που πρόκειται να ταξινοµηθούν αποτελούν µέρος µιας µεγαλύτερης
οµάδας δεδοµένων που ονοµάζονται εγγραφές (records). Μια εγγραφή συνήθως έχει
πολλά δεδοµένα, τα οποία ονοµάζονται πεδία και ένα από αυτά τα πεδία το κλειδί,
χρησιµοποιείται για την ταξινόµηση. Τα υπόλοιπα πεδία εκτός του κλειδιού, ονοµάζονται
συνήθως και δορυφορικά δεδοµένα. Τα δεδοµένα τα οποία ενδιαφέρουν την εφαρµογή
είναι τα δορυφορικά δεδοµένα, τα οποία µαζί µε το κλειδί, σε µια λειτουργία
ταξινόµησης θα πρέπει να αναδιαταχθούν.
Όταν τα δορυφορικά δεδοµένα είναι µικρά σε όγκο, η αλλαγή της θέσης της εγγραφής
κατά τη διάρκεια της ταξινόµησης δεν επιβαρύνει σηµαντικά το χρόνο του αλγόριθµου.
Όταν όµως τα δορυφορικά δεδοµένα είναι µεγάλα σε όγκο, εκείνο που ταξινοµούµε είναι
ένας πίνακας από δείκτες στις εγγραφές. Έτσι, θα ασχοληθούµε στη συνέχεια µόνο µε το
πρόβληµα της ταξινόµησης ενός πίνακα µε αριθµούς, αφήνοντας τις λεπτοµέρειες που
σχετίζονται µε τα δορυφορικά δεδοµένα.
Σήµερα έχουν αναπτυχθεί πολλοί αλγόριθµοι για την παράλληλη ταξινόµηση ενός
συνόλου αριθµών. Πολλοί από αυτούς απευθύνονται σε υπολογιστές µε ειδική
αρχιτεκτονική, πράγµα που κάνει αυτούς τους αλγόριθµους άχρηστους σε εφαρµογές µε
clusters.
Στο κεφάλαιο αυτί θα ασχοληθούµε µε τεχνικές παράλληλης ταξινόµησης που
προορίζονται για εµπορικά παράλληλα συστήµατα, θα θεωρήσουµε εσωτερικές
ταξινοµήσεις, δηλαδή περιπτώσεις που το σύνολο των στοιχείων που πρόκειται να
ταξινοµηθεί χωρά στην κύρια µνήµη των υπολογιστών( σε αντίθεση, στις εξωτερικές
ταξινοµήσεις, τα στοιχεία είναι τόσο πολλά που αυτό δεν είναι εφικτό). Επιπλέον, οι
αλγόριθµοι που θα δούµε , επιταχύνουν την ταξινόµηση κάνοντας σύγκριση σε ζευγάρια
αριθµών.
Quicksort
Ο αλγόριθµος Quicksort ανακαλύφθηκε από τον C.A.D.Hoare γύρω στα 50 χρόνια πριν.
Η βασική ιδέα του αλγόριθµου είναι η εξής: έχοντας µια λίστα µε αριθµούς ο αλγόριθµος
επιλέγει έναν αριθµό, τον άξονα (pivot), και δηµιουργεί δύο υπολίστες: στη µία
τοποθετούνται όλοι οι αριθµοί µικρότεροι ή ίσοι του άξονα, και στην άλλη όλοι οι
167
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
αριθµοί µεγαλύτεροι του άξονα. Στη συνέχεια ο αλγόριθµος λειτουργεί αναδροµικά στις
δύο υπολίστες. Ο αλγόριθµος τελειώνει ενώνοντας τις δύο υπολίστες µε τον άξονα.
Για παράδειγµα, ας δούµε πως λειτουργεί ο αλγόριθµος για να ταξινοµήσει τους αριθµούς
[79,17,14,65,89,4,95,22,63,11]. Θα υποθέσουµε ότι αλγόριθµος επιλέγει πάντα τον πρώτο
αριθµό της λίστας να είναι ο άξονας. Έτσι αρχικά ο άξονας είναι το 79 ,η λίστα µε τους
µικρούς αριθµούς είναι η [17,14,65,4,22,63,11] και η λίστα µε τους µεγάλους αριθµούς
είναι η [89,95]. Ο αλγόριθµος αναδροµικά θα προσπαθήσει να ταξινοµήσει αυτές τις δύο
λίστες.
Η αναδροµική εκτέλεση του αλγόριθµου στη λίστα [17,14,65,4,22,63,11] θα αφαιρέσει
από τη λίστα το 17 του άξονα και θα δηµιουργήσει τις λίστες [14,4,11] µε τους µικρούς
αριθµούς και τη λίστα [22,63,65] µε τους µεγάλους. Αναδροµικά, θα ταξινοµηθούν και
αυτές οι λίστες.
Ο αναδροµικός αλγόριθµος είναι βέβαιο ότι θα τερµατιστεί γιατί κάθε φορά το µήκος της
λίστας µειώνεται. Αν µια λίστα έχει ένα στοιχείο, τότε το στοιχείο αυτό γίνεται ο άξονας
και ο αλγόριθµος τελειώνει. Κάθε φορά που επιστρέφει ο αλγόριθµος συνενώνει τη λίστα
µε τους µικρούς αριθµούς, τον άξονα και τη λίστα µε τους µεγάλους αριθµούς. Έτσι, η
κλήση Q(17,14,65,4,22,63,11), θα δηµιουργήσει τις κλήσεις Q(14,4,11) και
Q(65,22,63).Η κλήση Q(14,4,11) θα επιστρέψει την λίστα [4,11,14] ενώ η κλίση
Q(65,22,63) θα επιστρέψει την λίστα [22,63,65].
Συνενώνοντας αυτές τις λίστες , η Q (4,11,14,17,22,63,65) θα επιστρέψει την λίστα
[4,11,14,17,22,63,65]. Στο παρακάτω σχήµα φαίνεται η αναδροµική λειτουργία του
αλγόριθµου.
Σχήµα 1. Αναδροµική εκτέλεση του quicksort για την ταξινόµηση 10 αριθµών.
168
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ένας παράλληλος quicksort
Ο quicksort είναι ο πιο γρήγορος αλγόριθµος για σειριακή ταξινόµηση (κατά µέση τιµή
αφού εξαρτάται από την τιµή του άξονα). Είναι καλό λοιπόν να προσπαθήσουµε να
παραλληλοποιήσουµε τον γρηγορότερο σειριακό αλγόριθµο. Επιπλέον, υπάρχει στον
quicksort ενσωµατωµένη µια δυνατότητα παραλληλισµού , αφού κάθε φορά η
ταξινόµηση των δύο υπολογιστών µπορεί να γίνει παράλληλα.
Ορισµός της ταξινόµησης σε παράλληλο υπολογιστή
Ας δούµε λίγο τι εννοούµε όταν λέµε ότι θα ταξινοµήσουµε ένα σύνολο αριθµών σε έναν
παράλληλο υπολογιστή. Λογικά, θα έπρεπε αρχικά να έχουµε τους αριθµούς σε έναν
επεξεργαστή και στο τέλος του αλγόριθµου να έχουµε τους αριθµούς αυτούς
ταξινοµηµένους στον ίδιο επεξεργαστή. Αυτό όµως δηµιουργεί πρόβληµα στην
κλιµάκωση του αλγόριθµου, αφού το σύνολο των αριθµών πρέπει να χωρά στην µνήµη
ενός υπολογιστή (παρ’ όλο που έχουµε στη διάθεσή µας πολλούς υπολογιστές και τις
αντίστοιχες µνήµες).
Μια καλύτερη αντιµετώπιση θα ήταν να είχαµε αρχικά κατανεµηµένους οµοιόµορφα
τους αριθµούς στους p διαθέσιµους υπολογιστές και στο τέλος του αλγόριθµου να είχαµε
πάλι τους αριθµούς κατανεµηµένους αλλά, οι αριθµοί σε κάθε υπολογιστή να είναι
ταξινοµηµένοι και όλοι οι αριθµοί στον υπολογιστή pi να είναι µικρότεροι ή ίσοι από
τους αριθµούς στον pi+1 υπολογιστή. Αυτό θα έλυνε και το πρόβληµα της κλιµάκωσης.
Ανάπτυξη του αλγόριθµου
Επειδή κάθε φορά ο quicksort καλεί τον εαυτό του δύο φορές (για τις δύο υπολίστες που
δηµιουργεί) θα θεωρήσουµε ότι ο αριθµός των υπολογιστών είναι δύναµη του 2.
Ας δούµε λίγο στο σχήµα 2. Αρχικά ο αριθµός είναι στους p υπολογιστές. Κάθε
υπολογιστής εφαρµόζει τον αλγόριθµο quicksort στους αριθµούς που διαθέτει και
δηµιουργεί τις λίστες µε τους µικρούς και τους µεγάλους αριθµούς. Στη συνέχεια οι
υπολογιστές που η ταυτότητά τους είναι µικρότερη του p/2 ανταλλάζουν τις λίστες µε
τους µικρότερους αριθµούς µε τις λίστες µε τους µεγάλους αριθµούς των υπολογιστών µε
ταυτότητα µεγαλύτερη ή ίση από p/2. Αυτό έχει σαν αποτέλεσµα, όλοι οι υπολογιστές µε
ταυτότητα µικρότερη του p/2 να έχουν µεγαλύτερους αριθµούς από αυτούς που έχουν οι
υπολογιστές µε ταυτότητα µικρότερη του p/2.
Ο αλγόριθµος τώρα µπορεί να εφαρµοστεί αναδροµικά στα δύο υποσύνολα του
υπολογιστή. Αυτό που θα προκύψει τελικά είναι κάθε υπολογιστής να έχει ένα
ταξινοµηµένο σύνολο αριθµών και κάθε υπολογιστής pi να έχει µεγαλύτερους αριθµούς
από τον pi+1
169
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 2. Σχηµατική αναπαράσταση της εφαρµογής του quicksort σε έναν παράλληλο
υπολογιστή.
Ανάλυση
Ο αλγόριθµος θα ήταν ιδανικός, αν υπήρχε κάποιος τρόπος να εξασφαλιστεί ότι κάθε
υπολογιστής θα είχε τον ίδιο όγκο στοιχείων να διαχειριστεί. Αυτό όµως εξαρτάται από
την τιµή του άξονα. Είδαµε στο προηγούµενο παράδειγµα ότι στη λίστα
[79,17,14,65,89,4,95,22,63,11] η επιλογή του 79 σαν άξονα οδηγεί στη λίστα
[17,14,65,4,22,63,11] µε 7 στοιχεία και στη λίστα [89,95] µε 2 στοιχεία.
Αντί λοιπόν να επιλέγουµε τυχαία έναν αριθµό σαν άξονα, θα ήταν καλύτερα να
επιλέξουµε τη µέση τιµή της λίστας. Αυτή είναι και η βελτίωση που θα προσπαθήσουµε
να κάνουµε στην επόµενη παράγραφο.
170
∆.Σ. Βλάχος και Θ.Η. Σίµος
Hyperquicksort
Περιγραφή του αλγόριθµου
Ο αλγόριθµος Hyperquicksort ανακαλύφθηκε από τον Wagar και προσπαθεί να επιλύσει
το πρόβληµα της ανοµοιόµορφης κατανοµής του υπολογιστικού φόρτου που
παρατηρείται στον quicksort. Αρχικά κάθε υπολογιστής ταξινοµεί µε τον quicksort το
σύνολο µε τους αριθµούς που διαθέτει. Όπως και στον quicksort, πρέπει να
χρησιµοποιηθεί ένας αριθµός άξονας για να χωριστούν σε δύο οµάδες οι αριθµοί, στους
µικρούς και στους µεγάλους. Η διεργασία που είναι υπεύθυνη να παρέχει αυτόν τον
αριθµό στις υπόλοιπες διεργασίες, µπορεί να χρησιµοποιήσει σαν άξονα το µεσαίο
αριθµό της λίστας που διαθέτει. Η τιµή αυτή θα οδηγήσει σε πιο οµοιόµορφη κατανοµή
των αριθµών στις δύο υπολίστες. Τα επόµενα βήµατα είναι τα ίδια µε αυτά του quicksort.
Κάθε διεργασία διαιρεί τους αριθµούς που διαθέτει σε δύο υπολίστες και στη συνέχεια
ανταλλάσσει τη µία από αυτές µε µία υπολίστα από άλλη διεργασία.
Εκείνο που πρέπει να κάνει επιπλέον η Hyperquicksort, είναι ότι µετά την ανταλλαγή,
πρέπει να ενώσει τις δύο υπολίστες που διαθέτει( τη µία που έλαβε και την άλλη που είχε
από πριν) σε µια ταξινοµηµένη λίστα, έτσι ώστε η διεργασία που θα είναι υπεύθυνη να
παράγει τον αριθµό άξονα στη συνέχεια να βρει το σωστό αριθµό που θα είναι στη µέση
της λίστας.
Μετά από logp τέτοια βήµατα , ο αρχικός υπερκύβος των p διεργασιών θα έχει διαιρεθεί
σε logp υπερκύβους και κάθε διεργασία θα διαθέτει µικρότερους (ή µεγαλύτερους)
αριθµούς από την επόµενή της.
Επειδή σε κάθε βήµα κάθε διεργασία συνενώνει και ταξινοµεί τις δύο λίστες που
διαθέτει, δεν υπάρχει ανάγκη να κλιθεί στο τέλος του αλγόριθµου η quicksort.
Στο σχήµα 3 φαίνεται ένα παράδειγµα εφαρµογής του Hyperquicksort(14.3)
171
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 3. Σχηµατική αναπαράσταση των διαφόρων βηµάτων της εφαρµογής του
αλγόριθµου hyperquicksort για ένα δείγµα 32 αριθµών σε έναν παράλληλο υπολογιστή µε 4
επεξεργαστές.
Ο Hyperquicksort υποθέτει πως ο αριθµός των διεργασιών είναι µία δύναµη του 2.
Ανάλυση ισο-απόδοσης
Ας υπολογίσουµε τώρα τη µετρική της ισο-απόδοσης του Hyperquicksort. Στην αρχή του
αλγόριθµου, κάθε διεργασία έχει το πολύ n/p στοιχεία. Θα θεωρούµε πάντα ότι το
n>>p. Η αναµενόµενη πολυπλοκότητα της εφαρµογής του quicksort θα είναι:
172
∆.Σ. Βλάχος και Θ.Η. Σίµος
n
n
Θ( log )
p
p
Θεωρώντας ότι σε κάθε βήµα κάθε διεργασία κρατά n/2p στοιχεία και µεταδίδει n/2p
στοιχεία , ο αναµενόµενος αριθµός συγκρίσεων για την συνένωση των δύο λιστών θα
είναι n/p. Αφού αυτό το βήµα θα εκτελεστεί για υπερκύβους διαστάσεων logp, (logp)-1,
...,1, ο αναµενόµενος αριθµός συγκρίσεων θα είναι Θ(n/p) logp και ο αναµενόµενος
αριθµός συγκρίσεων καθ’ όλη τη διάρκεια του αλγόριθµου θα είναι:
n

Θ  (log n + log p) 
p

Αφού οι διεργασίες είναι λογικά οργανωµένες σε έναν υπερκύβο d διαστάσεων η
µετάδοση του αριθµού άξονα χρειάζεται χρόνο Θ(d). Από την άλλη επειδή n>> p ο
χρόνος µετάδοσης του αριθµού άξονα θα είναι αµελητέος συγκρινόµενος µε το χρόνο για
την ανταλλαγή των υπολογιστών µεταξύ των διεργασιών. Αφού κάθε διεργασία στέλνει
n/2p ταξινοµηµένες τιµές ο χρόνος θα είναι Θ(n/p) . Υπάρχουν logp επαναλήψεις. Έτσι ο
αναµενόµενος επικοινωνιακός χρόνος θα είναι Θ(n logp/p).
Ο σειριακός αλγόριθµος έχει πολυπλοκότητα n logn. Ο επικοινωνιακός φόρτος του
παράλληλου αλγόριθµου θα είναι p φορές ο αναµενόµενος επικοινωνιακός χρόνος,
δηλαδή θα είναι Θ(n logp). Έτσι η µετρική ισο-απόδοσης θα είναι:
n log n ≥ Cn log p ⇒ log n ≥ C log p ⇒ n ≥ p C
Η ανάγκη σε µνήµη είναι Μ(n)= n. Έτσι η συνάρτηση κλιµάκωσης είναι
M ( p C ) / p = p C −1
Η τιµή του C καθορίζει τις δυνατότητες κλιµάκωσης του αλγόριθµου. Αν C>2, αυτές οι
δυνατότητες είναι µικρές.
Υπάρχει όµως άλλος ένας παράγοντας που µειώνει τη δυνατότητα κλιµάκωσης του
Hyperquicksort. Στην ανάλυσή µας υποθέσαµε πως ο µεσαίος αριθµός που επιλέγεται
είναι πάντα και ο ιδανικός αριθµός-άξονας, αυτός δηλαδή που διαιρεί κάθε λίστα σε δύο
ίσες σε µήκος υπολίστες. Αυτό όµως δεν είναι αλήθεια και οδηγεί σε άνιση κατανοµή του
υπολογιστικού φόρτου και αύξηση του επικοινωνιακού χρόνου.
Σχήµα 4. Αναπαράσταση των επικοινωνιών στον hyperquicksort όταν έχουµε 8 διεργασίες
και log8=3 βήµατα.
173
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σε γενικές γραµµές, ο Hyperquicksort έχει δύο αδυναµίες που περιορίζουν την
χρησιµότητα του. Κατ’ αρχήν ο αναµενόµενος αριθµός που ένας αριθµός άξονας περνά
από µια διεργασία σε µια άλλη είναι (logp)/2.
Αυτό δηµιουργεί έναν επιπλέον επικοινωνιακό χρόνο που δεν θα υπήρχε αν ο αριθµός
άξονας έφτανε αµέσως στον τελικό του προορισµό. Επιπλέον η επιλογή του αριθµού
άξονα δεν εξασφαλίζει οµοιοµορφία στα µεγέθη των λιστών, πράγµα που οδηγεί σε άνιση
κατανοµή του υπολογιστικού φόρτου και αύξηση του επικοινωνιακού χρόνου.
Αυτά τα δύο προβλήµατα θα λυθούν µε την επόµενη τεχνική ταξινόµησης που θα
παρουσιάσουµε, την τεχνική µε κανονική δειγµατοληψία.
Παράλληλη ταξινόµηση µε κανονική δειγµατοληψία
Η παράλληλη ταξινόµηση µε κανονική δειγµατοληψία έχει τρία πλεονεκτήµατα σε σχέση
µε τον Hyperquicksort.
•
∆ιατηρεί πιο οµοιόµορφα κατανεµηµένους τους αριθµούς στις διάφορες
διεργασίες
•
Αποφεύγει επαναλαµβανόµενες µεταδόσεις των αριθµών
•
∆εν απαιτεί οι διεργασίες να είναι δύναµη του 2
Περιγραφή του αλγόριθµου
Η παράλληλη ταξινόµηση µε κανονική δειγµατοληψία υλοποιείται σε 4 φάσεις όπως
φαίνεται στο σχήµα 5.
Σχήµα 5. Σχηµατική αναπαράσταση της λειτουργίας της παράλληλης ταξινόµησης µε
κανονική δειγµατοληψία όταν έχουµε 27 αριθµούς σε 3 επεξεργαστές.
Στη φάση 1, κάθε διεργασία ταξινοµεί τους  n/p αριθµούς που διαθέτει µε την
εφαρµογή του quicksort. Κάθε διεργασία επιλέγει p στοιχεία µε τοπικούς δείκτες 0, n /p2,
174
∆.Σ. Βλάχος και Θ.Η. Σίµος
2n /p2 ,..., (p -1)n /p2 σαν ένα κανονικό δείγµα των στοιχείων που διαθέτει.
Στη φάση 2 µια διεργασία συλλέγει τα κανονικά δείγµατα (συνολικό µήκος p2 στοιχείων)
και τα ταξινοµεί σε µια λίστα. Από αυτή τη λίστα επιλέγει p-1 αριθµούς άξονες µε
τοπικούς δείκτες p+p/2-1, 2p+p/2-1,... (p -1)p+p/2-1. Αφού µεταδώσει αυτούς τους
αριθµούς στις άλλες διεργασίες κάθε διεργασία διαιρεί τους αριθµούς που διαθέτει σε p
τµήµατα σύµφωνα µε τους p-1 αριθµούς άξονες.
Στο τρίτο βήµα, η διεργασία i κρατάει το i τµήµα που διαθέτει και µεταδίδει κάθε τµήµα
της j στη διεργασία j (για κάθε j≠ i).
Τέλος, στο τέταρτο βήµα, κάθε διεργασία συνενώνει και ταξινοµεί τα p τµήµατα που
διαθέτει σε µια λίστα. Στο τέλος αυτής της φάσης η ταξινόµηση έχει επιτευχθεί.
Από θεωρητικούς υπολογισµούς έχει προκύψει ότι σε κάθε περίπτωση, µια διεργασία θα
έχει το πολύ 2n /p αριθµούς, δηλαδή διπλάσιους από αυτό που στην ιδανική περίπτωση
θα της αναλογούσε. Στην πράξη όµως, αν οι αριθµοί είναι οµοιόµορφα κατανεµηµένοι ,
οι αριθµοί που θα έχει η κάθε διεργασία θα είναι πολύ κοντά στο n /p.
Ανάλυση ισο-απόδοσης
Ας υπολογίσουµε τώρα τη µετρική της ισο-απόδοσης του αλγόριθµου που αναπτύξαµε.
Στη φάση 1 κάθε διεργασία ταξινοµεί n /p στοιχεία. Ο χρόνος που χρειάζεται για να γίνει
αυτό είναι Θ{(n/p log (n/p)}. Στο τέλος της φάσης 1 µια διεργασία συλλέγει p στοιχεία
από τις p-1 υπόλοιπες διεργασίες. Το πλήθος των στοιχείων είναι µικρό άρα µπορούµε να
λάβουµε υπ’ όψιν µας µόνο το χρόνο προετοιµασίας των µηνυµάτων και έτσι ο
συνολικός χρόνος θα είναι Θ(logp).
Στη φάση 2, µια διεργασία ταξινοµεί τα p2 δείγµατα και έτσι απαιτεί χρόνο:
Θ( p 2 log p 2 ) = Θ( p 2 log p)
Στη φάση 3 κάθε διεργασία χρησιµοποιεί τους αριθµούς άξονες για να χωρίσει τους
αριθµούς που διαθέτει σε p τµήµατα. Στη συνέχεια υλοποιείται µια καθολική
επικοινωνία. Θεωρώντας ότι οι αριθµοί είναι οµοιόµορφα κατανεµηµένοι ο συνολικός
αριθµός που κάθε διεργασία στέλνει είναι
( p − 1)n / p 2 ≈ n / p
Αφού n>>p, ο επικοινωνιακός χρόνος κυριαρχείται από το χρόνο που κάνουν να
µεταδοθούν τα δεδοµένα. Αν υποθέσουµε επίσης, ότι το δίκτυο διασύνδεσης των p
υπολογιστών υποστηρίζει την ταυτόχρονη µετάδοση p µηνυµάτων (π. χ υπερδέντρο), ο
χρόνος θα είναι Θ(n/p).
Στη φάση 4 κάθε διεργασία συνενώνει p ταξινοµηµένες υπολίστες. Θεωρώντας και πάλι
ότι τα µήκη των υπολιστών είναι παρόµοια, ο χρόνος που απαιτείται γι αυτό είναι
175
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Θ(n / p log n / p + p 2 log p + n / p log p)
Αφού n>>p η παραπάνω έκφραση µπορεί να απλοποιηθεί στην εξής :
(
Θ n (log n + log p
p
)
Ο συνολικός επικοινωνιακός χρόνος είναι :
(
Θ n + log p
p
)
Και πάλι επειδή n>>p η παραπάνω έκφραση υλοποιείται στην
Θ( n / p )
Ο επιπλέον επικοινωνιακός φόρτος θα είναι :
pΘ(n / p) = Θ(n)
Προσθέτοντας σ’ αυτόν και τον επιπλέον χρόνο για τη συνένωση των λιστών επί του
αριθµού των διεργασιών , δηλαδή Θ (n logp).
Η µετρική ισο-απόδοσης θα είναι:
n log n ≥ Cn log p ⇒ log n ≥ C log p ⇒ n ≥ p C
Η συνάρτηση κλιµάκωσης θα είναι :
M ( p C ) / p = p C −1
ίδια µ’ αυτή του Hyperquicksort αλλά εδώ έχουµε εξασφαλίσει ότι ο υπολογιστικός
φόρτος θα είναι οµοιόµορφα κατανεµηµένος.
176
∆.Σ. Βλάχος και Θ.Η. Σίµος
Συνδυαστική
αναζήτηση
Εισαγωγή
Οι συνδυαστικοί αλγόριθµοι εκτελούν υπολογισµούς σε διακριτές και πεπερασµένες
µαθηµατικές δοµές. Η συνδυαστική αναζήτηση είναι η διαδικασία της εύρεσης µιας ή
περισσοτέρων βέλτιστων ή σχεδόν βέλτιστων λύσεων σε ένα πεπερασµένο και διακριτό
πρόβληµα. Μερικές εφαρµογές της συνδυαστικής αναζήτησης είναι:
•
VLSI σχεδίαση
•
Ροµποτική
•
Απόδειξη θεωρηµάτων
•
Θεωρία παιγνίων κ.α.
Υπάρχουν δύο ειδών προβλήµατα συνδυαστικής αναζήτησης. Ένας αλγόριθµος
απόφασης προσπαθεί να βρει µια λύση που ικανοποιεί κάποιους περιορισµούς. Η
απάντηση σε ένα πρόβληµα απόφασης είναι είτε ‘ναι’ που σηµαίνει ότι µια λύση υπάρχει
είτε ‘όχι’ που σηµαίνει το αντίθετο. Ας δούµε ένα παράδειγµα ενός προβλήµατος
απόφασης: ‘Υπάρχει τρόπος να δροµολογήσουµε το βραχίονα ενός ροµπότ έτσι ώστε να
επισκεφθεί ένα σύνολο από προκαθορισµένες θέσεις χωρίς να µετακινηθεί πάνω από 15
µέτρα;’
Από την άλλη µεριά, ένας αλγόριθµος που λύνει ένα πρόβληµα βελτιστοποίησης πρέπει
να βρει µια λύση που ελαχιστοποιεί (ή µεγιστοποιεί) µια συνάρτηση κόστους (ή
ποιότητας). Ένα παράδειγµα ενός προβλήµατος βελτιστοποίησης είναι: ‘Να βρεθεί η πιο
κοντινή δροµολόγηση του βραχίονα ενός ροµπότ έτσι ώστε να επισκεφθεί ένα σύνολο από
προκαθορισµένες θέσεις’.
Στο κεφάλαιο αυτό θα εξετάσουµε τεσσάρων ειδών συνδυαστικούς αλγόριθµους που
χρησιµοποιούνται για προβλήµατα απόφασης και προβλήµατα βελτιστοποίησης. Αυτοί οι
αλγόριθµοι είναι οι ‘διαίρει και βασίλευε (divide and conquer)’, ‘αντίστροφης
πορείας (backtrack)’, ‘διακλάδωσης και προσκόλλησης (branch and bound)’ και
‘άλφα-βήτα αναζήτηση (alpha-beta searching)’.
∆ιαφορετικοί αλγόριθµοι εξερευνούν διαφορετικά δέντρα αναζήτησης. Σε κάθε
περίπτωση, η ρίζα ενός δέντρου αναζήτησης αναπαριστά το αρχικό πρόβληµα που πρέπει
να λυθεί. Οι κόµβοι του δέντρου (όχι όµως τα φύλλα) χαρακτηρίζονται από διαφορετικές
ιδιότητες, ανάλογα µε τον τύπο του δέντρου αναζήτησης. Ένας κόµβος AND σηµαίνει ότι
το υπό-πρόβληµα το οποία αναπαριστά έχει λύση όταν και µόνο όταν όλα τα υπόπροβλήµατα που αναπαρίστανται από τους κόµβους παιδιά του έχουν λύση. Ένας κόµβος
177
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
OR σηµαίνει ότι το υπό-πρόβληµα το οποίο αναπαριστά έχει λύση αν τουλάχιστον ένα
από τα υπό-προβλήµατα που αναπαρίστανται από τους κόµβους-παιδιά του έχει λύση.
Ένα µικτό δέντρο είναι αυτό που έχει και τα δύο είδη κόµβων. Οι διάφοροι τύποι
δέντρων αναζήτησης φαίνονται στο παρακάτω σχήµα.
Σχήµα 1. Οι διάφοροι τύποι δέντρων αναζήτησης.
Ένας αλγόριθµος ‘διαίρει και βασίλευε’ λειτουργεί πάνω σε ένα δέντρο AND αφού η
λύση που αναζητά βρίσκεται συνδυάζοντας τις λύσεις των υπό-προβληµάτων. Ένα OR
δέντρο χρησιµοποιείται στους αλγόριθµους ‘αντιστροφής πορείας’ και ‘διακλάδωσης και
προσκόλλησης’. Τέλος, παραδείγµατα µικτών δέντρων βρίσκουµε στη θεωρία παιγνίων.
∆ιαίρει και βασίλευε
Η τεχνική του διαίρει και βασίλευε διαιρεί το αρχικό πρόβληµα σε υπό-προβλήµατα,
βρίσκει τη λύση στα υπό-προβλήµατα και στο τέλος συνδυάζει αυτές τις λύσεις για την
απάντηση του προβλήµατος. Η µεθοδολογία είναι από τη φύση της αναδροµική, αφού η
ίδια τεχνική µπορεί να εφαρµοστεί για κάθε ένα από τα υπό-προβλήµατα. Η µέθοδος
quicksort που αναπτύχθηκε στο προηγούµενο κεφάλαιο είναι ένα παράδειγµα αλγόριθµου
‘διαίρει και βασίλευε’.
Η τεχνική µπορεί να αναπαρασταθεί µε ένα δέντρο AND του οποίου κάθε εσωτερικός
κόµβος αντιστοιχεί στα διάφορα υπό-προβλήµατα που πρέπει να λυθούν. Η πιο
συνηθισµένη τεχνική υλοποίησης του ‘διαίρει και βασίλευε’ σε έναν παράλληλο
υπολογιστή είναι αυτή που τόσο το αρχικό πρόβληµα όσο και η τελική λύση είναι όσον
το δυνατόν περισσότερο οµοιόµορφα κατανεµηµένη στους επεξεργαστές.
Αντιστροφή πορείας
Η τεχνική της αντιστροφής πορείας είναι µια µεθοδολογία για την επίλυση συνδυαστικών
προβληµάτων βελτιστοποίησης που βασίζονται στην αναζήτηση ‘πρώτα σε βάθος (depthfirst search)’ για τον έλεγχο υποψήφιων λύσεων. Με δεδοµένο ένα πρόβληµα (η ρίζα του
δέντρου αναζήτησης), η τεχνική της αντιστροφής πορείας δηµιουργεί υπό-προβλήµατα
(κόµβοι-παιδιά) και µεταθέτει τον έλεγχο σε αυτούς τους κόµβους. Η διαδικασία αυτή
συνεχίζεται αναδροµικά, και µόνο όταν έχουν ελεγχθεί όλα τα παιδιά, ο έλεγχος
µεταφέρεται στον πατρικό κόµβο.
Παράδειγµα
Θεωρείστε το πρόβληµα της κατασκευής ενός σταυρόλεξου. Με δεδοµένο ένα άδειο
σταυρόλεξο (σχήµα 2α) και ένα σύνολο από λέξεις, ο στόχος µας είναι να
αντιστοιχίσουµε λέξεις σε κάθε οριζόντια και κάθετη οµάδα λευκών τετραγώνων µε
178
∆.Σ. Βλάχος και Θ.Η. Σίµος
µήκος µεγαλύτερο ή ίσο του δύο (σχήµα 2b). Αυτό είναι ένα παράδειγµα προβλήµατος
απόφασης. Με άλλα λόγια ‘υπάρχει τρόπος να γεµίσουµε τα άδεια τετράγωνα
χρησιµοποιώντας λέξεις από ένα δοσµένο σύνολο;’.
Σχήµα 2. Η συµπλήρωση ενός άδειου σταυρόλεξου από ένα δοσµένο σύνολο λέξεων είναι
ένα πρόβληµα απόφασης που µπορεί να λυθεί µε την τεχνική της αντιστροφής πορείας.
Η φιλοσοφία της δηµιουργίας του σταυρόλεξου είναι η γνωστή. Θα ονοµάζουµε µια λέξη
‘ηµιτελή’ αν της λείπει τουλάχιστον ένα γράµµα. Ο αλγόριθµος λειτουργεί ως εξής:
Αναζητούµε πρώτα τη µεγαλύτερη ηµιτελή λέξη (στην αρχή όλες οι λέξεις είναι
ηµιτελείς) και προσπαθούµε να βρούµε µια λέξη από το σύνολο των δοσµένων λέξεων
που να χωρά στα αντίστοιχα τετράγωνα. Αν βρεθούν πολλές τέτοιες λέξεις (έχουµε
πολλούς κόµβους-παιδιά στο δέντρο αναζήτησης) τότε επιλέγουµε µία από αυτές στην
τύχη. Τώρα προχωρούµε µε αναδροµικό τρόπο. Θεωρώντας ότι η λέξη που επιλέξαµε
ήταν η κατάλληλη, αναζητούµε την επόµενη µεγαλύτερη ηµιτελή λέξη και ψάχνουµε να
βρούµε µια λέξη από το σύνολο των λέξεων που να χωρά ακριβώς. Ο αλγόριθµος
σταµατά είτε όταν έχουν συµπληρωθεί όλες οι λέξεις είτε όταν δεν µπορούµε να βρούµε
καµιά λέξη από το σύνολο των λέξεων για να προχωρήσουµε. Στην πρώτη περίπτωση το
πρόβληµα έχει απαντηθεί θετικά ενώ στη δεύτερη αρνητικά. Σε περίπτωση αρνητικής
απάντησης ο αναδροµικός αλγόριθµος θα επιλέξει µια νέα λέξη εκτελώντας την πρώτη
διαδικασία από τη στοίβα των διαδικασιών (κάθε φορά που µια αναδροµική διαδικασία
καλεί τον εαυτό της, η διεύθυνση επιστροφής µπαίνει σε µια στοίβα). Στο σχήµα 3
φαίνεται ένα στιγµιότυπο από την εκτέλεση του αλγόριθµου.
179
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 3. Στιγµιότυπο του αλγόριθµου αντιστροφής πορείας για το γέµισµα ενός
σταυρόλεξου από ένα δοσµένο σύνολο λέξεων.
180
∆.Σ. Βλάχος και Θ.Η. Σίµος
Ας δούµε λίγο πως µπορούµε να αναπαραστήσουµε αυτήν την τεχνική µε ένα δέντρο
αναζήτησης. Το πρόβληµα που έχουµε να λύσουµε αντιστοιχεί στη ρίζα του δέντρου. Στο
πρώτο βήµα, όταν αναζητούµε τη µεγαλύτερη ηµιτελή λέξη, για κάθε µια από τις
επιλογές που προκύπτουν δηµιουργείται ένας κόµβος-παιδί που αντιστοιχεί τώρα στο νέο
πρόβληµα της συµπλήρωσης του σταυρόλεξου αν έχουµε δοσµένη µια λέξη. Η
διαδικασία µε αναδροµικό τρόπο επαναλαµβάνεται για κάθε έναν από τους κόµβους που
δηµιουργήθηκε. Προφανώς πρόκειται για ένα δέντρο OR, αφού µόλις βρεθεί µια λύση
(δηλαδή κάποιο από τα υπό-προβλήµατα –παιδιά έχει απαντηθεί) τότε το πρόβληµα
απαντάται χωρίς να υπάρχει ανάγκη ελέγχου και των υπόλοιπων επιλογών.
Ας δούµε λίγο τώρα την πολυπλοκότητα του αλγόριθµου. Αν ο µέσος αριθµός κλάδων
είναι b και το δέντρο έχει βάθος k, τότε στη χειρότερη περίπτωση χρειαζόµαστε:
1 + b + b 2 + ... + b k =
b k +1 − b
= Θ(b k )
b −1
δηλαδή εκθετικό χρόνο.
Και ενώ οι ανάγκες σε χρόνο είναι κάπως απογοητευτικές, οι ανάγκες σε µνήµη
αυξάνουν γραµµικά µε το βάθος του δέντρου (Θ(k)) γιατί µόνο ο κλάδος που ελέγχεται
κάθε φορά είναι ανάγκη να βρίσκεται στην κύρια µνήµη. Συνεπώς, η χρήση του
αλγόριθµου περιορίζεται από το χρόνο που χρειάζεται να τρέξει και όχι από τις ανάγκες
του σε µνήµη.
Παράλληλη αναζήτηση µε αντιστροφή πορείας
Παρ’ όλο που ο χρόνος της τεχνικής της αντιστροφής πορείας είναι εκθετικός,
παρατηρούµε ότι η αναζήτηση σε έναν κλάδο του δέντρου είναι εντελώς ανεξάρτητη από
την αναζήτηση σε κάποιον άλλο κλάδο. Αυτή η παρατήρηση φανερώνει και τις µεγάλες
δυνατότητες παραλληλισµού που έχει αυτή η τεχνική.
Σχήµα 4. Κάθε διεργασία στην παράλληλη υλοποίηση της αντιστροφής πορείας µπορεί να
ελέγχει ένα υπό-δέντρο του δέντρου αναζήτησης.
181
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Ο προφανής τρόπος για να παραλληλοποιήσουµε το πρόβληµα είναι να αφήσουµε τον
έλεγχο των διαφόρων υπό-δέντρων σε ξεχωριστές διεργασίες. Ας υποθέσουµε ότι το
δέντρο έχει b κόµβους παιδιά ανά κόµβο και έχει βάθος d . Έστω ότι ο αριθµός των
επεξεργαστών p που χρησιµοποιούµε είναι p=bk Έτσι αφήνουµε όλες τις διεργασίες να
προχωρήσουν µαζί ως το επίπεδο κ και από κει, κάθε διεργασία ελέγχει ένα από τα pυπό-δέντρα. Αν επιπλέον το d>>2k τότε η επιτάχυνση θα είναι πολύ κοντά στο p.
Αν δεν υπάρχει κάποιο k ώστε p= bk, τότε αφήνουµε όλες τις διεργασίες να φτάσουν σε
κάποιο επίπεδο m και τότε κάθε διεργασία αναλαµβάνει τον έλεγχο µιας οµάδας υπόδέντρων.
Ας δούµε το παράδειγµα ,όπου b=3, d=10, και p=5.Αν ο παράλληλος αλγόριθµος αρχίζει
από το πρώτο επίπεδο, τότε µόνο µια διεργασία λειτουργεί και η επιτάχυνση είναι 1. Αν ο
παράλληλος αλγόριθµος αρχίζει από το δεύτερο επίπεδο, όπου έχουµε 3 υπό-δέντρα, τότε
µόνο τρεις επεξεργαστές χρειάζονται και η επιτάχυνση είναι 3. Αν ο παράλληλος
αλγόριθµος αρχίζει από το τρίτο επίπεδο τότε έχουµε 9 υπό-δέντρα και 4 επεξεργαστές
που αναλαµβάνουν να ελέγχουν από 2 υπό-δέντρα και ένας επεξεργαστής 1 υπό-δέντρο.
Η επιτάχυνση θα είναι κοντά στο 9/2=4.5.
Αν το βάθος του δέντρου είναι πολύ µεγάλο, τότε µπορούµε να ξεκινήσουµε τον
παράλληλο αλγόριθµο σε µεγαλύτερο επίπεδο, όπου έχουµε περισσότερα υπό-δέντρα τα
οποία θα κατανεµηθούν µε µεγαλύτερη οµοιοµορφία ανάµεσα στους επεξεργαστές. Στο
παρακάτω σχήµα φαίνεται η επιτάχυνση που µπορεί να επιτευχθεί σαν συνάρτηση του
βάθους του δέντρου.
Σχήµα 5. Μέγιστη επιτάχυνση που µπορεί να επιτευχθεί σε 5 επεξεργαστές σαν συνάρτηση
του βάθους του δέντρου αναζήτησης. Κάθε κόµβος στο δέντρο έχει 3 κλαδιά.
Η δυσκολία που συναντάται στην εφαρµογή του αλγόριθµου είναι ότι τις περισσότερες
φορές τα δέντρα αναζήτησης που προκύπτουν από τα προβλήµατα στην πράξη, δεν έχουν
σταθερό αριθµό κλάδων ανά κόµβο. Αυτό αντιµετωπίζεται µε το να αφήνουµε το
σειριακό αλγόριθµο να προχωρά σε µεγαλύτερο βαθµό, αυξάνοντας έτσι τον αριθµό των
υπό-δέντρων που θα ελεγχθούν από κάθε επεξεργαστή. Έτσι αυξάνεται η πιθανότητα να
υπάρχει µεγαλύτερη οµοιοµορφία στην κατανοµή του υπολογιστικού φόρτου.
182
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παρακάτω δίνουµε τον ψευδοκώδικα για τη κατανοµή κλάδων αναζήτησης ενός
ανοµοιογενούς δέντρου σε p διεργασίες. Κάθε διεργασία ψάχνει το δέντρο ως το επίπεδο
cutoff_depth. Στη συνέχεια η διεργασία –i αναλαµβάνει τα υπό-δέντρα των οποίων ο
αύξων αριθµός mod p=i (υποθέτουµε πως τα υπό-δέντρα που έχουν προκύψει έχουν
αριθµηθεί).
Τέλος, στο σχήµα 6, φαίνονται σκιασµένα τα υπό-δέντρα που ελέγχει η διεργασία 0, όπου
έχει υποτεθεί ότι p=4 και cutoff_depth=3.
//Global variables
cutoff_count
//Count of nodes at cutoff depth
cutoff_depth
//Depth at which subtrees are divided among
//processes
depth
//Depth up to which the tree is searched
moves
//Records position in search tree
id
//Process rank
p
//Number of processes
Parallel_Backtrack(board,level)
if level=depth then
if board represents a solution to the problem then
Print_Solutions(moves)
endif
else
if level=cutoff_depth then
cutoff_count++
if cutoff_count mod p # id then
return
endif
endif
possible_moves=Count_Moves(board)
for i=1 to possible_moves do
Make_Move(board,i)
moves[level]=i
Parallel_Backtrack(board,level+1)
Unmake_Move(board,i)
endfor
endif
return
183
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Σχήµα 6. Παράλληλη αναζήτηση σε ένα ανοµοιόµορφο δέντρο. Στο παράδειγµα υπάρχουν 4
επεξεργαστές και έχουν σηµειωθεί σκιασµένα τα υπό-δέντρα που ελέγχει η διεργασία 0.
Κατανεµηµένη ανίχνευση τερµατισµού
Κάθε διεργασία που εκτελεί τον παράλληλο αλγόριθµο της προηγούµενης παραγράφου
τερµατίζεται αφού έχει ελέγξει πλήρως τα υπό-δέντρα που της αντιστοιχούν. Με άλλα
λόγια ο αλγόριθµος ψάχνει και βρίσκει όλες τις λύσεις. Σε µερικές περιπτώσεις αυτό είναι
καλό, γιατί από τις λύσεις που βρέθηκαν µπορεί να επιλεγεί η βέλτιστη, αλλά σε κάποιες
άλλες αρκεί να βρεθεί µια λύση (η πρώτη που θα βρεθεί).
Ας υποθέσουµε ότι µας ενδιαφέρει η περίπτωση που θέλουµε να βρούµε µια µόνο λύση
του προβλήµατος (την πρώτη που θα βρεθεί). Ένας τρόπος, για να τερµατίσει το
πρόγραµµα µόλις κάποια διεργασία βρει αυτή τη λύση είναι ο εξής: Η διεργασία που
βρίσκει τη λύση στέλνει ένα µήνυµα σε όλες τις άλλες διεργασίες µε tag, ας πούµε,
MSG_FOUND. Όλες οι διεργασίες ελέγχου χρησιµοποιώντας τη συνάρτηση MPI_IProbe
αν έχει ληφθεί κάποιο µήνυµα µε tag MSG_FOUND και αποστολέα
MPI_ΑΝΥ_SOURCE. Η συνάρτηση MPI_IProbe επιστρέφει αµέσως και δε δηµιουργεί
καθυστέρηση στην εκτέλεση µιας διεργασίας.
Προκύπτει όµως τώρα το εξής πρόβληµα: Ας υποθέσουµε ότι δύο διεργασίες βρίσκουν
από µια λύση και στέλνουν το µήνυµα MSG_FOUND πριν λάβουν η µία από την άλλη το
αντίστοιχο µήνυµα. Έστω ότι η µία από τις από τις δύο τερµατίζεται πριν φτάσει το
µήνυµα. Τότε αυτό θα δηµιουργήσει ένα λάθος εκτέλεσης , αφού θα έχει σταλεί ένα
µήνυµα σε µια διεργασία που πλέον δεν υφίσταται. Η ασφαλής λύση αυτού του
προβλήµατος λέγεται κατανεµηµένη ανίχνευση τερµατισµού και ανακαλύφθηκε γύρω
στο 1980 από τους Dijkstra, Seizen και Gasteren. Στο παρακάτω σχήµα φαίνεται η
λειτουργία αυτού του αλγόριθµου.
184
∆.Σ. Βλάχος και Θ.Η. Σίµος
Σχήµα 7. Ο αλγόριθµος των Dijkstra, Seizen και Gasteren για τον ασφαλή τερµατισµό των
διεργασιών.
Κάθε διεργασία έχει ένα χρώµα και έναν αριθµό µηνυµάτων. Όταν µια διεργασία ξεκινά,
είναι άσπρη και ο αριθµός µηνυµάτων είναι 0. Μια διεργασία γίνεται µαύρη όταν στέλνει
ή λαµβάνει µήνυµα. Όταν µια διεργασία στέλνει ένα µήνυµα αυξάνει τον αριθµό
µηνυµάτων και όταν λαµβάνει τον µειώνει. Η βασική ιδέα του αλγόριθµου είναι ότι όταν
όλες οι διεργασίες είναι άσπρες και ο συνολικός αριθµός µηνυµάτων είναι 0, τότε
ξέρουµε ότι δεν υπάρχουν εναποµείναντα µηνύµατα και οι διεργασίες µπορούν να
τερµατιστούν.
185
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
186
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παράρτηµα Ι
Τα βοηθητικά αρχεία mympi.h και
mympi.c
#ifndef _MYMPI_
#define _MYMPI_
//--------------------------------------------------------------------------// Message types
#define DATA_MSG
0
#define PROMPT_MSG
1
#define RESPONSE_MSG 2
//--------------------------------------------------------------------------// Error codes
#define OPEN_FILE_ERROR -1
#define MALLOC_ERROR -2
#define TYPE_ERROR
-3
//--------------------------------------------------------------------------// Macros
#define MIN(a,b) ((a)<(b)?(a):(b))
#define MAX(a,b) ((a)>(b)?(a):(b))
//
#define BLOCK_LOW(id,p,n) ((id)*(n)/(p))
#define BLOCK_HIGH(id,p,n) (BLOCK_LOW((id)+1,p,n)-1)
#define BLOCK_SIZE(id,p,n) (BLOCK_HIGH(id,p,n)BLOCK_LOW(id,p,n)+1)
#define BLOCK_OWNER(j,p,n) (((p)*((j)+1)-1)/(n))
//
#define PTR_SIZE
(sizeof(void*))
#define CEILING(i,j)
(((i)+(j)-1)/(j))
//--------------------------------------------------------------------------// Function definitions
//---------------------- General functions
void terminate(int,char*);
void* my_malloc(int,int);
//---------------------- Data distribution functions
void
replicate_block_vector(void*,int,void*,MPI_Datatype,MPI_Comm
);
void create_mixed_xfer_arrays(int,int,int,int**,int**);
187
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
void create_uniform_xfer_arrays(int,int,int,int**,int**);
//---------------------- Input functions
void
read_checkboard_matrix(char*,void***,void**,MPI_Datatype,int
*,int*,MPI_Comm);
void
read_col_striped_matrix(char*,void***,void**,MPI_Datatype,in
t*,int*,MPI_Comm);
void
read_row_striped_matrix(char*,void***,void**,MPI_Datatype,in
t*,int*,MPI_Comm);
void
read_block_vector(char*,void**,MPI_Datatype,int*,MPI_Comm);
void
read_replicated_vector(char*,void**,MPI_Datatype,int*,MPI_Co
mm);
//----------------------- Output functions
void
print_checkboard_matrix(void**,MPI_Datatype,int,int,MPI_Comm
);
void
print_col_striped_matrix(void**,MPI_Datatype,int,int,MPI_Com
m);
void
print_row_striped_matrix(void**,MPI_Datatype,int,int,MPI_Com
m);
void print_block_vector(void*,MPI_Datatype,int,MPI_Comm);
void
print_replicated_vector(void*,MPI_Datatype,int,MPI_Comm);
#endif
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<mpi.h>
"mympi.h"
//------------------------ Local functions
// Function get_size.
// It returns the size in bytes of a specific MPI data type.
// Supported types are MPI_BYTE,MPI_DOUBLE,MPI_FLOAT and
MPI_INT.
// In case of an unknown type, aborts.
int get_size(MPI_Datatype t) {
switch (t) {
case MPI_BYTE:
return sizeof(char);
188
∆.Σ. Βλάχος και Θ.Η. Σίµος
case MPI_DOUBLE:
return sizeof(double);
case MPI_FLOAT:
return sizeof(float);
case MPI_INT:
return sizeof(int);
default:
printf("Error: Unrecognized argument to
'get_size'\n");
fflush(stdout);
MPI_Abort(MPI_COMM_WORLD,TYPE_ERROR);
}
return 0;
}
// Function my_malloc.
// It allocates 'bytes' number of bytes.
// If an error occurs, it prints a debug message with
// the process id who failed to allocate memory.
void* my_malloc(int id, int bytes) {
void* buffer;
if ((buffer=malloc((size_t)bytes))==NULL) {
printf("Error: Malloc failed in process %d\n",id);
fflush(stdout);
MPI_Abort(MPI_COMM_WORLD,MALLOC_ERROR);
}
return buffer;
}
//----------------------- General functions
// Function terminate.
// It finalizes the MPI machine. Process 0 prints
// an error message.
// The return code is -1 to notify for the error.
void terminate(int id, char* error_message) {
if (!id) {
printf("Error: %s\n",error_message);
fflush(stdout);
}
MPI_Finalize();
exit(-1);
}
//------------------------ Data distributing functions
// Function create_mixed_xfer_arrays.
// It creates the count and displacement arrays
// needed for scatter and gather operations.
void create_mixed_xfer_arrayes(
int
id,
//IN - Process id
int
p,
//IN - Number of processes
int
n,
//IN - Total number of elements
189
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
{
int**
int**
int
count,
//OUT - Array of counts
disp) //OUT - Array of displacements
i;
*count=my_malloc(id,p*sizeof(int));
*disp=my_malloc(id,p*sizeof(int));
(*count)[0]=BLOCK_SIZE(0,p,n);
(*disp)[0]=0;
for (i=1;i<p;i++) {
(*disp)[i]=(*disp)[i-1]+(*count)[i-1];
(*count)[i]=BLOCK_SIZE(i,p,n);
}
}
// Function create_uniform_xfer_arrays.
// It creates the count and displacement arrays
// needed for scatter and gather operations.
void create_uniform_xfer_arrayes(
int
id,
//IN - Process id
int
p,
//IN - Number of processes
int
n,
//IN - Total number of elements
int**
count,
//OUT - Array of counts
int**
disp) //OUT - Array of displacements
{
int
i;
*count=my_malloc(id,p*sizeof(int));
*disp=my_malloc(id,p*sizeof(int));
(*count)[0]=BLOCK_SIZE(id,p,n);
(*disp)[0]=0;
for (i=1;i<p;i++) {
(*disp)[i]=(*disp)[i-1]+(*count)[i-1];
(*count)[i]=BLOCK_SIZE(id,p,n);
}
}
// Function replicate_block_vector.
// It replicates a distributed vector to all the
// processes in a communicator
void replicate_block_vector
(
void
*ablock, //IN - Block-distributed vector
int
n,
//IN - Elements in vector
void
*arep,
//OUT - Replicated vector
MPI_Datatype
dtype,
//IN - Element type
MPI_Comm comm)
//IN - Communicator
{
int*
cnt;
int*
disp;
int
id;
int
p;
MPI_Comm_size(comm,&p);
190
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Comm_rank(comm,&id);
crete_mixed_xfer_arrays(id,p,n,&cnt,&disp);
MPI_Allgatherv(ablock,cnt[id],dtype,arep,cnt,disp,dtype,c
omm);
free(cnt);
free(disp);
}
//------------------------ Input functions
// Function read_cheackboard_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of the matrix
// to the MPI processes.
// The number of the processes must be a square number.
void read_checkboard_matrix
(
char*
s,
//IN - File name
void***
subs,
//OUT - 2D array
void**
storage, //OUT - Array storage area
MPI_Datatype dtype,
//IN - Element type
int*
m,
//OUT - Array rows
int*
n,
//OUT - Array cols
MPI_Comm grid_comm) //IN - Communicator
{
void*
buffer;
int
coord[2];
int
datum_size;
int
dest_id;
int
grid_coord[2];
int
grid_id;
int
grid_period[2];
int
grid_size[2];
int
i,j,k;
FILE*
infileptr;
void*
laddr;
int
local_cols;
int
local_rows;
int
p;
void*
raddr;
MPI_Status status;
MPI_Comm_rank(grid_comm,&grid_id);
MPI_Comm_size(grid_comm,&p);
datum_size=get_size(dtype);
// Process 0 opens file, reads 'm' and 'n' and broadcasts
these to
191
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
// the other processes.
if (grid_id==0)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*m=0;
else {
fread(m,sizeof(int),1,infileptr);
fread(n,sizeof(int),1,infileptr);
}
}
MPI_Bcast(m,1,MPI_INT,0,grid_comm);
if (!(*m))
MPI_Abort(MPI_COMM_WORLD,OPEN_FILE_ERROR);
MPI_Bcast(n,1,MPI_INT,0,grid_comm);
);
// Each process determines the size of the submatrix
// that it is responsible for.
MPI_Cart_get(grid_comm,2,grid_size,grid_period,grid_coord
local_rows=BLOCK_SIZE(grid_coord[0],grid_size[0],*m);
local_cols=BLOCK_SIZE(grid_coord[1],grid_size[1],*n);
// Dynamically allocate 2D matrix
*storage=my_malloc(grid_id,local_rows*local_cols*datum_si
ze);
*subs=(void**)my_malloc(grid_id,local_rows*PTR_SIZE);
for(i=0;i<local_rows;i++)
);
(*subs)[i]=&(((char*)(*storage))[i*local_cols*datum_size]
// Grid process 0 reads in the matrix one row at a time
// and distributes it in the proper process
if (grid_id==0)
buffer=my_malloc(grid_id,*n*datum_size);
// For each row of processes ...
for (i=0;i<grid_size[0];i++) {
coords[0]=i;
// For each matrix row controlled by this process..
for (j=0;j<BLOCK_SIZE(i,grid_size[0],*m);j++)
{
if (grid_id==0)
fread(buffer,datum_size,*n,infileptr);
// Distribute it
for (k=0;k<grid_size[1];k++) {
coords[1]=k;
// Find address of first element to send
raddr=buffer+BLOCK_LOW(k,grid_size[1],*n)*datum_size;
// Determine the grid id of the process getting
the subrow
MPI_Cart_rank(grid_comm,coords,&dest_id);
192
∆.Σ. Βλάχος και Θ.Η. Σίµος
//Send..
if (grid_id==0)
{
if (dest_id==0)
{
laddr=(*subs)[j];
memcpy(laddr,raddr,local_rows*datum_size);
}else{
MPI_Send(raddr,BLOCK_SIZE(k,grid_size[1],*n),dtype,dest_i
d,0,grid_comm);
}
}else if (grid_id==dest_id)
{
MPI_Recv(((*subs)[j],local_cols,dtype,0,0,grid_comm,&stat
us);
}
}
}
}
if (grid_id==0)
free(buffer);
}
// Function read_col_striped_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of columns
// to the MPI processes.
void read_col_striped_matrix (
char*
s,
//IN - File name
void***
subs,
//OUT - 2D submatrix indices
void**
storage, //OUT - Submatrix storage area
MPI_Datatype dtype,
//IN - Matrix element type
int*
m,
//OUT - Matrix rows;
int*
n,
//OUT - Matrix cols
MPI_Comm comm)
//IN - Communicator
{
}
// Function read_row_striped_matrix.
// It reads a matrix from a file. The first two elements
// of the file are integers indicating the number
// of rows 'm' and number of columns 'n'.
// The m*n values of the matrix are stored in row-major
order.
// The function next allocates blocks of rows
// to the MPI processes.
void read_row_striped_matrix(
char*
s,
//IN - File name
193
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
{
void***
subs,
//OUT - 2D submatrix indices
void**
storage, //OUT - Submatrix storage area
MPI_Datatype dtype,
//IN - Matrix element type
int*
m,
//OUT - Matrix rows;
int*
n,
//OUT - Matrix cols
MPI_Comm comm)
//IN - Communicator
int
datum_size; //Size of matrix elements
int
i;
int
id;
//Process rank
FILE*
infileptr; //Input file pointer
int
local_rows; //Rows on this process
int
p;
//Number of processes
MPI_Status status;
//Result of receive
int
x;
//Result of read
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
datum_size=get_size(dtype);
//Process (p-1) opens file and reads matrix dimensions
if (id==p-1)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*m=0;
else {
fread(m,sizeof(int),1,infileptr);
fread(n,sizeof(int),1,infileptr);
}
}
MPI_Bcast(m,1,MPI_INT,p-1,comm);
//Check for abort
if(!(*m))
MPI_Abort(MPI_COMM_WORLD,OPEN_FILE_ERROR);
//Ok. Broadcast columns
MPI_Bcast(n,1,MPI_INT,p-1,comm);
//
local_rows=BLOCK_SIZE(id,p,*m);
//Dynamically allocate matrix
*storage=(void*)my_malloc(id,local_rows*(*n)*datum_size);
*subs=(void**)my_malloc(id,local_rows*PTR_SIZE);
for(i=0;i<local_rows;i++)
(*subs)[i]=&(((char*)(*storage))[i*(*n)*datum_size]);
//Process (p-1) reads a block and transmits it
if(id==p-1) {
for(i=0;i<p-1;i++)
{
194
∆.Σ. Βλάχος και Θ.Η. Σίµος
x=fread(*storage,datum_size,BLOCK_SIZE(i,p,*m)*(*n),infil
eptr);
MPI_Send(*storage,BLOCK_SIZE(i,p,*m)*(*n),dtype,i,DATA_MS
G,comm);
}
x=fread(*storage,datum_size,local_rows*(*n),infileptr);
fclose(infileptr);
}
else
MPI_Recv(*storage,local_rows*(*n),dtype,p1,DATA_MSG,comm,&status);
}
// Function read_block_vector.
// It reads a vector from a file. The first element
// of the file is an integer indicating the length
// of the vector.
// The function next allocates blocks of the elements of the
vector
// to the MPI processes.
void read_block_vector (
char*
s,
//IN - File name
void**
v,
//OUT - Subvector
MPI_Datatype
dtype,
//IN - Element type
int*
n,
//OUT - Vector length
MPI_Comm
coom)
//IN - Communicator
{
int
datum_size;
int
i;
FILE*
infileptr;
int
local_els;
MPI_Status status;
int
id;
int
p;
int
x;
datum_size=get_size(dtype);
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
// Process p-1 opens file, determine vector length and
// broadcasts this to the other processes.
if (id==p-1)
{
infileptr=fopen(s,"r");
if (infileptr==NULL)
*n=0;
else
195
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
fread(n,sizeof(int),1,infileptr);
}
MPI_Bcast(n,1,MPI_INT,p-1,comm);
if (!*n) {
terminate(id,"Cannot open vector file");
}
local_els=BLOCK_SIZE(id,p,*n);
// Dynamically allocate vector space
*v=my_malloc(id,local_els*datum_size);
if (id==p-1)
{
for (i=0;i<p-1;i++) {
x=fread(*v,datum_size,BLOCK_SIZE(i,p,*n),infileptr);
MPI_Send(*v,BLOCK_SIZE(i,p,*n),dtype,i,DATA_MSG,comm);
}
x=fread(*v,datum_size,BLOCK_SIZE(id,p,*n),infileptr);
fclose(infileptr);
}else{
MPI_Recv(*v,BLOCK_SIZE(id,p,*n),dtype,p1,DATA_MSG,comm,&status);
}
}
// Function read_replicated_vector.
// It opens a file, reads the contents of a vector
// and replicates it among all the processes.
void read_replicated_vector
(
char*
s,
//IN - File name
void**
v,
//OUT - Vector
MPI_Datatype
dtype,
//IN - Vector type
int*
n,
//OUT - Vector length
MPI_Comm
comm)
//IN - Communicator
{
int
datum_size;
int
i;
int
id;
FILE*
infileptr;
int
p;
MPI_Comm_rank(comm,&id);
MPI_Comm_size(comm,&p);
datum_size=get_size(dtype);
if (id==(p-1)) {
infileptr=fopen(s,"r");
if (infileptr==NULL)
*n=0;
else
fread(n,sizeof(int),1,infileptr);
}
MPI_Bcast(n,1,MPI_INT,p-1,comm);
196
∆.Σ. Βλάχος και Θ.Η. Σίµος
if (!*n)
terminate(id,"Cannot open vector file");
*v=my_malloc(id,*n*datum_size);
if (id==p-1)
{
fread(*v,datum_size,*n,infileptr);
fclose(infileptr);
}
MPI_Bcast(*v,*n,dtype,p-1,comm);
}
//--------------------------- Output functions
// Function print_submatrix
// Prints the elements of a double sybscripted matrix
void print_submatrix(
void
**a,
//IN - Double subscripted array
MPI_Datatype
dtype,
//IN - Type of array elements
int
rows,
//IN - Row number of matrix
int
cols)
//IN - Col number of matrix
{
int
i,j;
for (i=0;i<rows;i++) {
for (j=0;j<cols;j++) {
switch (dtype) {
case MPI_DOUBLE:
printf("%6.3f ",((double**)a)[i][j]);
break;
case MPI_FLOAT:
printf("%6.3f ",((float**)a)[i][j]);
break;
case MPI_INT:
printf("%6d ",((int**)a)[i][j]);
break;
default:
break;
}
}
putchar('\n');
}
}
// Function print_subvector
// Prints the elements of a singly sybscripted vector
void print_subvector (
void*
a,
//IN - Vector pointer
MPI_Datatype
dtype,
//IN - Element type
int
n)
//IN - Vector size
{
int
i;
for (i=0;i<n;i++) {
197
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
if (dtype==MPI_DOUBLE)
printf("%6.3f ",((double*)a)[i]);
else {
if (dtype==MPI_FLOAT)
printf("%6.3f ",((float*)a)[i]);
else if (dtype==MPI_INT)
printf("%6d ",((int*)a)[i]);
}
}
}
// Function print_row_striped_matrix
// Prints the contents of a matrix that is
// row-distributed among the processes
void print_row_striped_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
comm)
//IN - Communicator
{
MPI_Status
status;
//Result of receive
void
*bstorage; //Elements received from another
process
void
**b;
//2D array indexing 'bstorage'
int
datum_size; //Size of matrix elements
int
i;
int
id;
//Process rank
int
local_rows; //This proc's rows
int
max_block_size;
int
prompt;
//Dummy variable
int
p;
//Number of processes
MPI_Comm_rank(comm,&id);
MPI_Comm_size(comm,&p);
local_rows=BLOCK_SIZE(id,p,m);
//Process 0 starts printing
if (!id) {
print_submatrix(a,dtype,local_rows,n);
if (p>1) {
datum_size=get_size(dtype);
max_block_size=BLOCK_SIZE(p-1,p,m);
bstorage=my_malloc(id,max_block_size*n*datum_size);
b=(void**)my_malloc(id,max_block_size*PTR_SIZE);
for (i=0;i<max_block_size;i++)
b[i]=&(((char*)bstorage)[i*n*datum_size]);
//
for (i=1;i<p;i++) {
MPI_Send(&prompt,1,MPI_INT,i,PROMPT_MSG,MPI_COMM_WORLD);
MPI_Recv(bstorage,BLOCK_SIZE(i,p,m)*n,dtype,
198
∆.Σ. Βλάχος και Θ.Η. Σίµος
i,RESPONSE_MSG,MPI_COMM_WORLD,&status);
print_submatrix(b,dtype,BLOCK_SIZE(i,p,m),n);
}
free(b);
free(bstorage);
}
putchar('\n');
}
else
{
MPI_Recv(&prompt,1,MPI_INT,0,PROMPT_MSG,MPI_COMM_WORLD,&s
tatus);
MPI_Send(*a,local_rows*n,dtype,0,RESPONSE_MSG,MPI_COMM_WO
RLD);
}
}
// Function print_col_striped_matrix
// Prints the contents of a matrix that is
// column-distributed among the processes
void print_col_striped_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
comm)
//IN - Communicator
{
}
// Function print_checkboard_matrix
// Prints the contents of a matrix that is
// distributed checkboard fashion among the processes
void print_checkboard_matrix (
void
**a,
//IN - 2D array
MPI_Datatype
dtype,
//IN - Matrix element type
int
m,
//IN - Number or rows
int
n,
//IN - Number of cols
MPI_Comm
grid_comm) //IN - Communicator
{
void*
buffer;
int
coords[2];
int
datum_size;
int
els;
int
grid_coords[2];
int
grid_id;
int
grid_period[2];
int
grid_size[2];
int
i,j,k;
void*
laddr;
int
local_cols;
199
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
int
int
MPI_Status
p;
src;
status;
MPI_Comm_rank(grid_comm,&grid_id);
MPI_Comm_size(grid_comm,&p);
datum_size=get_size(dtype);
MPI_Cart_get(grid_comm,2,grid_size,grid_period,grid_coord
s);
local_cols=BLOCK_SIZE(grid_coords[1],grid_size[1],n);
if (!grid_id)
buffer=my_malloc(grid_id,n*datum_size);
// For each row in the process grid..
for (i=0;i<grid_size[0],i++) {
coords[0]=i;
// For each matrix row in the current process
for (j=0;j<BLOCK_SIZE(i,grid_size[0],m);j++) {
// Collect the matrix row int the process 0
// and then print it
if (!grid_id) {
for (k=0;k<grid_size[1];k++) {
coords[1]=k;
MPI_Cart_rank(grid_comm,coords,&src);
els=BLOCK_SIZE(k,grid_size[1],n);
laddr=buffer+BLOCK_LOW(k,grid_size[1],n)*datum_size;
if (src==0) {
memcpy(laddr,a[j],els*datum_size);
}else {
MPI_Recv(laddr,els,dtype,src,0,grid_comm,&status);
}
}
print_subvector(buffer,dtype,n);
putchar('\n');
}else if (grid_coords[0]==i) {
MPI_Send(a[j],local_cols,dtype,0,0,grid_comm);
}
}
}
if (!grid_id) {
free(buffer);
putchar('\n');
}
}
// Function print_block_vector
// Prints the contents of a vector which is
// block distributed among the processes.
200
∆.Σ. Βλάχος και Θ.Η. Σίµος
void print_block_vector (
void*
v,
//IN - Address of vector
MPI_Datatype
dtype,
//IN - Element type
int
n,
//IN - Sizeof vector
MPI_Comm
comm)
//IN - Communicator
{
int
datum_size;
int
i;
int
prompt;
MPI_Status status;
void*
tmp;
int
id;
int
p;
MPI_Comm_size(comm,&p);
MPI_Comm_rank(comm,&id);
datum_size=get_size(dtype);
if (!id) {
print_subvector(v,dtype,BLOCK_SIZE(id,p,n));
if (p>1) {
tmp=my_malloc(id,BLOCK_SIZE(p-1,p,n)*datum_size);
for (i=1;i<p;i++) {
MPI_Send(&prompt,1,MPI_INT,i,PROMPT_MSG,comm);
MPI_Recv(tmp,BLOCK_SIZE(i,p[,n),dtype,i,RESPONSE_MSG,comm
,&status);
print_subvector(tmp,dtype,BLOCK_SIZE(i,p,n));
}
free(tmp);
}
printf("\n\n");
}else{
MPI_Recv(&prompt,1,MPI_INT,0,PROMPT_MSG,comm,&status);
MPI_Send(v,BLOCK_SIZE(id,p,n),dtype,0,RESPONSE_MSG,comm);
}
}
// Function print_replicated_vector.
// Prints a vector that is replicated among the
// proccesses of a communicator
void print_replicated_vector (
void*
v,
//IN - Vector
MPI_Datatype
dtype,
//IN - Element type
int
n,
//IN - Vector size
MPI_Comm
comm)
//IN - Communicator
{
int
id;
201
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
}
MPI_Comm_rank(comm,&id);
if (!id) {
print_subvector(v,dtype,n);
printf("\n\n");
}
202
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παράρτηµα ΙΙ
Οι συναρτήσεις MPI
Environment Management Routines
MPI_Abort
MPI_Errhandler_create
MPI_Errhandler_free
MPI_Errhandler_get
MPI_Errhandler_set
MPI_Error_class
MPI_Error_string
MPI_Finalize
MPI_Get_processor_name
MPI_Init
MPI_Initialized
MPI_Wtick
MPI_Wtime
Point-to-Point Communication Routines
MPI_Bsend
MPI_Bsend_init
MPI_Buffer_attach
MPI_Buffer_detach
MPI_Cancel
MPI_Get_count
MPI_Get_elements
MPI_Ibsend
MPI_Iprobe
MPI_Irecv
MPI_Irsend
MPI_Isend
MPI_Issend
MPI_Probe
MPI_Recv
MPI_Recv_init
MPI_Request_free
MPI_Rsend
MPI_Rsend_init
MPI_Send
MPI_Send_init
MPI_Sendrecv
MPI_Sendrecv_replace
MPI_Ssend
MPI_Ssend_init
MPI_Start
MPI_Startall
MPI_Test
MPI_Test_cancelled
MPI_Testall
MPI_Testany
MPI_Testsome
MPI_Wait
MPI_Waitall
MPI_Waitany
MPI_Waitsome
Collective Communication Routines
MPI_Allgather
MPI_Allgatherv
MPI_Allreduce
MPI_Alltoall
MPI_Alltoallv
MPI_Barrier
MPI_Bcast
MPI_Gather
MPI_Gatherv
MPI_Op_create
MPI_Op_free
MPI_Reduce
MPI_Reduce_scatter
MPI_Scan
MPI_Scatter
MPI_Scatterv
203
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Process Group Routines
MPI_Group_compare
MPI_Group_difference
MPI_Group_excl
MPI_Group_free
MPI_Group_incl
MPI_Group_intersection
MPI_Group_range_excl
MPI_Group_range_incl
MPI_Group_rank
MPI_Group_size
MPI_Group_translate_ranks
MPI_Group_union
MPI_Comm_compare
MPI_Comm_create
MPI_Comm_dup
MPI_Comm_free
MPI_Comm_group
MPI_Comm_rank
MPI_Comm_remote_group
MPI_Comm_remote_size
MPI_Comm_size
MPI_Comm_split
MPI_Comm_test_inter
MPI_Intercomm_create
MPI_Type_commit
MPI_Type_contiguous
MPI_Type_vector
MPI_Type_extent
MPI_Type_free
MPI_Type_hindexed
MPI_Type_hvector
MPI_Type_indexed
MPI_Type_lb
MPI_Type_size
MPI_Type_struct
MPI_Type_ub
MPI_Cart_coords
MPI_Cart_create
MPI_Cart_get
MPI_Cart_map
MPI_Cart_rank
MPI_Cart_shift
MPI_Cart_sub
MPI_Cartdim_get
MPI_Dims_create
MPI_Graph_create
MPI_Graph_get
MPI_Graph_map
MPI_Graph_neighbors
MPI_Graph_neighbors_count
MPI_Graphdims_get
MPI_Address
MPI_Attr_delete
MPI_Attr_get
MPI_Attr_put
MPI_Keyval_create
MPI_Keyval_free
MPI_Pack
MPI_Pack_size
MPI_Pcontrol
Communicators Routines
MPI_Intercomm_merge
Derived Types Routines
Virtual Topology Routines
MPI_Topo_test
Miscellaneous Routines
MPI_Unpack
204
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Abort
Forces all tasks of an MPI job to terminate.
#include <mpi.h>
int MPI_Abort(MPI_Comm comm,int errorcode);
Parameters
Comm
is the communicator of the tasks to abort. (IN)
errorcode is the error code returned to the invoking environment.(IN)
MPI_Errhandler_create
Registers a user-defined error handler.
#include <mpi.h>
int MPI_Errhandler_create(MPI_Handler_function *function,
MPI_Errhandler *errhandler);
Parameters
function
is a user-defined error handling procedure (IN)
errhandler is an MPI error handler (handle) (OUT)
MPI_Errhandler_free
Marks an error handler for deallocation.
#include <mpi.h>
int MPI_Errhandler_free(MPI_Errhandler *errhandler);
Parameters
errhandler is an MPI error handler (handle) (INOUT)
MPI_Errhandler_get
Gets an error handler associated with a communicator.
#include <mpi.h>
int MPI_Errhandler_get(MPI_Comm comm,MPI_Errhandler *errhandler);
Parameters
comm
is a communicator (handle) (IN)
errhandler is the MPI error handler currently associated with comm.(handle) (OUT)
MPI_Errhandler_set
Associates a new error handler with a communicator.
#include <mpi.h>
int MPI_Errhandler_set(MPI_Comm comm,MPI_Errhandler errhandler);
Parameters
comm
is a communicator (handle) (IN)
errhandler is a new MPI error handler for comm (handle) (IN)
MPI_Error_class
205
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Returns the error class for the corresponding error code.
#include <mpi.h>
int MPI_Error_class(int errorcode,int *errorclass);
Parameters
errorcode is the error code returned by an MPI routine (IN)
errorclass is the error class for errorcode (OUT)
MPI_Error_string
Returns the error string for a given error code.
#include <mpi.h>
int MPI_Error_string(int errorcode,char *string, int *resultlen);
Parameters
errorcode is the error code returned by an MPI routine (IN)
string
is the error message for the errorcode (OUT)
resultlen is the character length of string (OUT)
MPI_Finalize
Terminates all MPI processing.
#include <mpi.h>
int MPI_Finalize(void);
Parameters
MPI_Get_processor_name
Returns the name of the local processor.
#include <mpi.h>
int MPI_Get_processor_name(char *name,int *resultlen);
Parameters
name
is a unique specifier for the actual node (OUT)
resultlen specifies the printable character length of the result returned in name (OUT)
MPI_Init
Initializes MPI.
#include <mpi.h>
int MPI_Init(int *argc,char ***argv);
Parameters
argc and argv are the arguments passed to main. PE MPI does not examine or modify these
arguments when they are passed to MPI_INIT. In accordance with MPI-2, it is valid to pass
NULL in place of argc and argv.
MPI_Initialized
Determines whether MPI is initialized.
#include <mpi.h>
int MPI_Initialized(int *flag);
206
∆.Σ. Βλάχος και Θ.Η. Σίµος
Parameters
flag
is true if MPI_INIT was called; otherwise is false.
MPI_Wtick
Returns the resolution of MPI_WTIME in seconds.
#include <mpi.h>
double MPI_Wtick(void);
Parameters
MPI_Wtime
Returns the current value of time as a floating-point value.
#include <mpi.h>
double MPI_Wtime(void);
Parameters
MPI_Bsend
Performs a blocking buffered mode send operation.
#include <mpi.h>
int MPI_Bsend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of destination (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Bsend_init
Creates a persistent buffered mode send request.
#include <mpi.h>
int MPI_Bsend_init(void* buf,int count,MPI_Datatype datatype,int
dest,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements to be sent (integer) (IN)
datatype
is the type of each element (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Buffer_attach
Provides MPI with a buffer to use for buffering messages sent with MPI_BSEND and
MPI_IBSEND.
207
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
#include <mpi.h>
int MPI_Buffer_attach(void* buffer,int size);
Parameters
buffer
is the initial buffer address (choice) (IN)
size
is the buffer size in bytes (integer) (IN)
MPI_Buffer_detach
Detaches the current buffer.
#include <mpi.h>
int MPI_Buffer_detach(void* buffer,int *size);
Parameters
buffer
is the initial buffer address (choice) (OUT)
size
is the buffer size in bytes (integer) (OUT)
MPI_Cancel
Marks a nonblocking request for cancellation.
#include <mpi.h>
int MPI_Cancel(MPI_Request *request);
Parameters
request
is a communication request (handle) (IN)
MPI_Get_count
Returns the number of elements in a message.
#include <mpi.h>
int MPI_Get_count(MPI_Status *status,MPI_Datatype datatype,int
*count);
Parameters
status
is a status object (Status) (IN). Note that in FORTRAN a single status object is an
array of integers.
datatype
is the datatype of each message element (handle) (IN)
count
is the number of elements (integer) (OUT)
MPI_Get_elements
Returns the number of basic elements in a message.
#include <mpi.h>
int MPI_Get_elements(MPI_Status *status,MPI_Datatype datatype,int
*count);
Parameters
status
is a status of object (status) (IN). Note that in FORTRAN a single status object is
an array of integers.
datatype
is the datatype used by the operation (handle) (IN)
count
is an integer specifying the number of basic elements (OUT)
MPI_Ibsend
Performs a nonblocking buffered mode send operation.
208
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Ibsend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task in comm (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Iprobe
Checks to see if a message matching source, tag, and comm has arrived.
#include <mpi.h>
int MPI_Iprobe(int source,int tag,MPI_Comm comm,int *flag,MPI_Status
*status);
Parameters
source
is a source rank or MPI_ANY_SOURCE (integer) (IN)
tag
is a tag value or MPI_ANY_TAG (integer) (IN)
comm
is a communicator (handle) (IN)
flag
(logical) (OUT)
status
is a status object (Status) (OUT). Note that in FORTRAN a single status object is
an array of integers.
MPI_Irecv
Performs a nonblocking receive operation.
#include <mpi.h>
int MPI_Irecv(void* buf,int count,MPI_Datatype datatype,int
source,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the receive buffer (choice) (OUT)
count
is the number of elements in the receive buffer (integer) (IN)
datatype
is the datatype of each receive buffer element (handle) (IN)
source
is the rank of source or MPI_ANY_SOURCE (integer) (IN)
tag
is the message tag or MPI_ANY_TAG (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Irsend
Performs a nonblocking ready mode send operation.
#include <mpi.h>
int MPI_Irsend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm,MPI_Request *request);
209
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task in comm (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Isend
Performs a nonblocking standard mode send operation.
#include <mpi.h>
int MPI_Isend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm,MPI_request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task in comm (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Issend
Performs a nonblocking synchronous mode send operation.
#include <mpi.h>
int MPI_Issend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task in comm (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Probe
Waits until a message matching source, tag, and comm arrives.
#include <mpi.h>
int MPI_Probe(int source,int tag,MPI_Comm comm,MPI_Status *status);
Parameters
source
is a source rank or MPI_ANY_SOURCE (integer) (IN)
tag
is a source tag or MPI_ANY_TAG (integer) (IN)
comm
is a communicator (handle) (IN)
status
is a status object (Status) (OUT). Note that in FORTRAN a single status object is
210
∆.Σ. Βλάχος και Θ.Η. Σίµος
an array of integers.
MPI_Recv
Performs a blocking receive operation.
#include <mpi.h>
int MPI_Recv(void* buf,int count,MPI_Datatype datatype,int source,int
tag,MPI_Comm comm,MPI_Status *status);
Parameters
buf
is the initial address of the receive buffer (choice) (OUT)
count
is the number of elements to be received (integer) (IN)
datatype
is the datatype of each receive buffer element (handle) (IN)
source
is the rank of the source task in comm or MPI_ANY_SOURCE (integer) (IN)
tag
is the message tag or MPI_ANY_TAG (integer) (IN)
comm
is the communicator (handle) (IN)
status
is the status object (Status) (OUT). Note that in FORTRAN a single status object is
an array of integers.
MPI_Recv_init
Creates a persistent receive request.
#include <mpi.h>
int MPI_Recv_init(void* buf,int count,MPI_Datatype datatype,int
source,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the receive buffer (choice) (OUT)
count
is the number of elements to be received (integer) (IN)
datatype
is the type of each element (handle) (IN)
source
is the rank of source or MPI_ANY_SOURCE (integer) (IN)
tag
is the tag or MPI_ANY_TAG (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Request_free
Marks a request for deallocation.
#include <mpi.h>
int MPI_Request_free(int MPI_Request *request);
Parameters
request
is a communication request (handle) (INOUT)
MPI_Rsend
Performs a blocking ready mode send operation.
#include <mpi.h>
int MPI_Rsend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm);
Parameters
211
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of destination (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Rsend_init
Creates a persistent ready mode send request.
#include <mpi.h>
int MPI_Rsend_init(void* buf,int count,MPI_Datatype datatype,int
dest,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements to be sent (integer) (IN)
datatype
is the type of each element (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Send
Performs a blocking standard mode send operation.
#include <mpi.h>
int MPI_Send(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (non-negative integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task in comm(integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Send_init
Creates a persistent standard mode send request.
#include <mpi.h>
int MPI_Send_init(void* buf,int count,MPI_Datatype datatype,int
dest,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements to be sent (integer) (IN)
datatype
is the type of each element (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
212
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Sendrecv
Performs a blocking send and receive operation.
#include <mpi.h>
int MPI_Sendrecv(void* sendbuf,int sendcount,MPI_Datatype
sendtype,int dest,int sendtag,void *recvbuf,int
recvcount,MPI_Datatype recvtype,int source,int recvtag,MPI_Comm
comm,MPI_Status *status);
Parameters
sendbuf
is the initial address of the send buffer (choice) (IN)
sendcount
is the number of elements to be sent (integer) (IN)
sendtype
is the type of elements in the send buffer (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
sendtag
is the send tag (integer) (IN)
recvbuf
is the initial address of the receive buffer (choice) (OUT)
recvcount
is the number of elements to be received (integer) (IN)
recvtype
is the type of elements in the receive buffer (handle) (IN)
source
is the rank of the source task or MPI_ANY_SOURCE (integer) (IN)
recvtag
is the receive tag or MPI_ANY_TAG (integer) (IN)
comm
is the communicator (handle) (IN)
status
is the status object (Status) (OUT).
MPI_Sendrecv_replace
Performs a blocking send and receive operation using a common buffer.
#include <mpi.h>
int MPI_Sendrecv_replace(void* buf,int count,MPI_Datatype
datatype,int dest,int sendtag,int source,int recvtag,MPI_Comm
comm,MPI_Status *status);
Parameters
buf
is the initial address of the send and receive buffer (choice) (INOUT)
count
is the number of elements to be sent and received (integer) (IN)
datatype
is the type of elements in the send and receive buffer (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
sendtag
is the send message tag (integer) (IN)
source
is the rank of the source task or MPI_ANY_SOURCE (integer) (IN)
recvtag
is the receive message tag or MPI_ANY_TAGE (integer) (IN)
comm
is the communicator (handle) (IN)
status
is the status object (Status) (OUT).
MPI_Ssend
Performs a blocking synchronous mode send operation.
#include <mpi.h>
int MPI_Ssend(void* buf,int count,MPI_Datatype datatype,int dest,int
tag,MPI_Comm comm);
Parameters
213
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of each send buffer element (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Ssend_init
Creates a persistent synchronous mode send request.
#include <mpi.h>
int MPI_Ssend_init(void* buf,int count,MPI_Datatype datatype,int
dest,int tag,MPI_Comm comm,MPI_Request *request);
Parameters
buf
is the initial address of the send buffer (choice) (IN)
count
is the number of elements to be sent (integer) (IN)
datatype
is the type of each element (handle) (IN)
dest
is the rank of the destination task (integer) (IN)
tag
is the message tag (integer) (IN)
comm
is the communicator (handle) (IN)
request
is the communication request (handle) (OUT)
MPI_Start
Activates a persistent request operation.
#include <mpi.h>
int MPI_Start(MPI_Request *request);
Parameters
request
is a communication request (handle) (INOUT)
MPI_Startall
Activates a collection of persistent request operations.
#include <mpi.h>
int MPI_Startall(int count,MPI_request *array_of_requests);
Parameters
count
is the list length (integer) (IN)
array_of_requests is the array of requests (array of handle) (INOUT)
MPI_Test
Checks to see if a nonblocking request has completed.
#include <mpi.h>
int MPI_Test(MPI_Request *request,int *flag,MPI_Status *status);
Parameters
request
is the operation request (handle) (INOUT)
flag
true if operation completed (logical) (OUT)
status
status object (Status) (OUT).
MPI_Test_cancelled
Tests whether a nonblocking operation was cancelled.
214
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Test_cancelled(MPI_Status * status,int *flag);
Parameters
status
is a status object (Status) (IN).
flag
true if the operation was cancelled (logical) (OUT)
MPI_Testall
Tests a collection of nonblocking operations for completion.
#include <mpi.h>
int MPI_Testall(int count,MPI_Request *array_of_requests,int
*flag,MPI_Status *array_of_statuses);
Parameters
count
is the number of requests to test (integer) (IN)
array_of_requests is an array of requests of length count (array ofhandles) (INOUT)
flag
(logical) (OUT)
array_of_statuses is an array of status of length count objects (array of status) (OUT).
MPI_Testany
Tests for the completion of any nonblocking operation.
#include <mpi.h>
int MPI_Testany(int count,MPI_Request *array_of_requests,int
*index,int *flag,MPI_Status *status);
Parameters
count
is the list length (integer) (IN)
array_of_requests is the array of request (array of handles) (INOUT)
index
is the index of the operation that completed, or MPI_UNDEFINED is no
operation completed (OUT)
flag
true if one of the operations is complete (logical) (OUT)
status
status object (Status) (OUT).
MPI_Testsome
Tests a collection of nonblocking operations for completion.
#include <mpi.h>
int MPI_Testsome(int incount,MPI_Request *array_of_requests,int
*outcount,int *array_of_indices,MPI_Status *array_of_statuses);
Parameters
incount
is the length of array_of_requests (integer) (IN)
array_of_requests is the array of requests (array of handles) (INOUT)
outcount
is the number of completed requests (integer) (OUT)
array_of_indices is the array of indices of operations that completed (array of integers)
(OUT)
MPI_Wait
Waits for a nonblocking operation to complete.
215
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
#include <mpi.h>
int MPI_Wait(MPI_Request *request,MPI_Status *status);
Parameters
request
is the request to wait for (handle) (INOUT)
status
is the status object (Status) (OUT).
MPI_Waitall
Waits for a collection of nonblocking operations to complete.
#include <mpi.h>
int MPI_Waitall(int count,MPI_Request *array_of_requests,MPI_Status
*array_of_statuses);
Parameters
count
is the lists length (integer) (IN)
array_of_requests is an array of requests of length count (array of handles) (INOUT)
array_of_statuses is an array of status objects of length count (array of status) (OUT).
MPI_Waitany
Waits for any specified nonblocking operation to complete.
#include <mpi.h>
int MPI_Waitany(int count,MPI_Request *array_of_requests,int
*index,MPI_Status *status);
Parameters
count
is the list length (integer) (IN)
array_of_requests is the array of requests (array of handles) (INOUT)
index
ias the index of the handle for the operation that completed (integer) (OUT)
status
status object (Status) (OUT).
MPI_Waitsome
Waits for at least one of a list of nonblocking operations to complete.
#include <mpi.h>
int MPI_Waitsome(int incount,MPI_Request *array_of_requests,int
*outcount,int *array_of_indices,MPI_Status *array_of_statuses);
Parameters
incount
is the length of array_of_requests, array_of_indices, and array_of_statuses
(integer)(IN)
array_of_requests is an array of requests (array of handles) (INOUT)
outcount
is the number of completed requests (integer) (OUT)
array_of_indices is the array of indices of operations that completed (array of integers)
(OUT)
array_of_statuses is the array of status objects for operations that completed (array of
status) (OUT).
MPI_Allgather
Gathers individual messages from each task in comm and distributes the resulting message
to each task.
216
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Allgather(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int recvcount,MPI_Datatype recvtype,MPI_Comm
comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcount is the number of elements in the send buffer (integer)(IN)
sendtype
is the datatype of the send buffer elements (handle) (IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcount is the number of elements received from any task (integer) (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
comm
is the communicator (handle) (IN)
MPI_Allgatherv
Collects individual messages from each task in comm and distributes the resulting message
to all tasks. Messages can have different sizes and displacements.
#include <mpi.h>
int MPI_Allgatherv(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int *recvcounts,int *displs,MPI_Datatype
recvtype,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcount
is the number of elements in the send buffer (integer) (IN)
sendtype
is the datatype of the send buffer elements (handle) (IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcounts
integer array (of length groupsize) that contains the number of elements
received from each task (IN)
displs
integer array (of length groupsize). Entry I specifies the displacement (relative to
recvbuf) at which to place the incoming data from task i (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
comm
is the communictor (handle) (IN)
MPI_Allreduce
Applies a reduction operation to the vector sendbuf over the set of tasks specified by comm
and places the result in recvbuf on all of the tasks in comm.
#include <mpi.h>
int MPI_Allreduce(void* sendbuf,void* recvbuf,int count,MPI_Datatype
datatype,MPI_Op op,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
recvbuf
is the starting address of the receive buffer (choice) (OUT)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of elements in the send buffer (handle) (IN)
op
is the reduction operation (handle) (IN)
comm
is the communicator (handle) (IN)
217
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Alltoall
Sends a distinct message from each task to every task.
#include <mpi.h>
int MPI_Alltoall(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int recvcount,MPI_Datatype recvtype,MPI_Comm
comm):
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcount is the number of elements sent to each task (integer) (IN)
sendtype
is the datatype of the send buffer elements (handle) (IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcount is the number of elements received from any task (integer) (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
comm
is the communicator (handle) (IN)
MPI_Alltoallv
Sends a distinct message from each task to every task. Messages can have different sizes
and displacements.
#include <mpi.h>
int MPI_Alltoallv(void* sendbuf,int *sendcounts,int
*sdispls,MPI_Datatype sendtype,void* recvbuf,int *recvcounts,int
*rdispls,MPI_Datatype recvtype,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcounts integer array (of length groupsize) specifying the number of elements to send to
each task (IN)
sdispls
integer array (of length groupsize). Entry j specifies the displacement relative to
sendbuf from which to take the outgoing data destined for task j. (IN)
sendtype
is the datatype of the send buffer elements (handle)(IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcounts integer array (of length groupsize) specifying thenumber of elements to be
received from each task (IN)
rdispls
integer array (of length groupsize). Entry i specifiesthe displacement relative to
recvbuf at which to place the incoming data from task i. (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
comm.
is the communicator (handle) (IN)
MPI_Barrier
Blocks each task in comm until all tasks have called it.
#include <mpi.h>
int MPI_Barrier(MPI_Comm comm);
Parameters
comm
is a communicator (handle) (IN)
MPI_Bcast
Broadcasts a message from root to all tasks in comm.
218
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Bcast(void* buffer,int count,MPI_Datatype datatype,int
root,MPI_Comm comm);
Parameters
buffer
is the starting address of the buffer (choice) (INOUT)
count
is the number of elements in the buffer (integer) (IN)
datatype
is the datatype of the buffer elements (handle) (IN)
root
is the rank of the root task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Gather
Collects individual messages from each task in comm at the root task.
#include <mpi.h>
int MPI_Gather(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int recvcount,MPI_Datatype recvtype,int
root,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcount is the number of elements in the send buffer (integer) (IN)
sendtype
is the datatype of the send buffer elements (integer) (IN)
recvbuf
is the address of the receive buffer (choice, significant only at root) (OUT)
recvcount is the number of elements for any single receive(integer, significant only at root)
(IN)
recvtype
is the datatype of the receive buffer elements (handle,significant only at root) (IN)
root
is the rank of the receiving task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Gatherv
Collects individual messages from each task in comm at the root task. Messages can have
different sizes and displacements.
#include <mpi.h>
int MPI_Gatherv(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int recvcounts,int *displs,MPI_Datatype
recvtype,int root,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
sendcount
is the number of elements in the send buffer (integer) (IN)
sendtype
is the datatype of the send buffer elements (handle) (IN)
recvbuf
is the address of the receive buffer (choice,significant only at root) (OUT)
recvcounts
integer array (of length groupsize) that contains thenumber of elements
received from each task (significant only at root) (IN)
displs
integer array (of length groupsize). Entry ispecifies the displacement relative to
recvbuf at which to place the incoming data from task (significant only at root) (IN)
recvtype
is the datatype of the receive buffer elements(handle, significant only at root)
(IN)
219
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
root
is the rank of the receiving task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Op_create
Binds a user-defined reduction operation to an op handle.
#include <mpi.h>
int MPI_Op_create(MPI_User_function *function,int commute,MPI_Op
*op);
Parameters
function
is the user-defined reduction function (function) (IN)
commute
is true if commutative; otherwise it is false (IN)
op
is the reduction operation (handle) (OUT)
MPI_Op_free
Marks a user-defined reduction operation for deallocation.
#include <mpi.h>
int MPI_Op_free(MPI_Op *op);
Parameters
op
is the reduction operation (handle) (INOUT)
MPI_Reduce
Applies a reduction operation to the vector sendbuf over the set of tasks specified by comm
and places the result in recvbuf on root.
#include <mpi.h>
int MPI_Reduce(void* sendbuf,void* recvbuf,int count,MPI_Datatype
datatype,MPI_Op op,int root,MPI_Comm comm);
Parameters
sendbuf
is the address of the send buffer (choice) (IN)
recvbuf
is the address of the receive buffer (choice, significant only at root) (OUT)
count
is the number of elements in the send buffer (integer) (IN)
datatype
is the datatype of elements of the send buffer (handle) (IN)
op
is the reduction operation (handle) (IN)
root
is the rank of the root task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Reduce_scatter
Applies a reduction operation to the vector sendbuf over the set of tasks specified by comm
and scatters the result according to the values in recvcounts.
#include <mpi.h>
int MPI_Reduce_scatter(void* sendbuf,void* recvbuf,int
*recvcounts,MPI_Datatype datatype,MPI_Op op,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
recvbuf
is the starting address of the receive buffer (choice) (OUT)
recvcounts
integer array specifying the number of elements in result distributed to each
task. Must be identical on all calling tasks. (IN)
220
∆.Σ. Βλάχος και Θ.Η. Σίµος
datatype
is the datatype of elements in the input buffer (handle) (IN)
op
is the reduction operation (handle) (IN)
comm
is the communicator (handle) (IN)
MPI_Scan
Performs a parallel prefix reduction operation on data distributed across a group.
#include <mpi.h>
int MPI_Scan(void* sendbuf,void* recvbuf,int count,MPI_Datatype
datatype,MPI_Op op,MPI_Comm comm);
Parameters
sendbuf
is the starting address of the send buffer (choice) (IN)
recvbuf
is the starting address of the receive buffer (choice) (OUT)
count
is the number of elements in sendbuf (integer) (IN)
datatype
is the datatype of elements in sendbuf (handle) (IN)
op
is the reduction operation (handle) (IN)
comm
is the communicator (IN)
MPI_Scatter
Distributes individual messages from root to each task in comm.
#include <mpi.h>
int MPI_Scatter(void* sendbuf,int sendcount,MPI_Datatype
sendtype,void* recvbuf,int recvcount,MPI_Datatype recvtype,int
root,MPI_Comm comm);
Parameters
sendbuf
is the address of the send buffer (choice, significant only at root) (IN)
sendcount is the number of elements to be sent to each task (integer, significant only at
root) (IN)
sendtype
is the datatype of the send buffer elements (handle, significant only at root) (IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcount is the number of elements in the receive buffer (integer) (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
root
is the rank of the sending task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Scatterv
Distributes individual messages from root to each task in comm. Messages can have different
sizes and displacements.
#include <mpi.h>
int MPI_Scatterv(void* sendbuf,int *sendcounts,int
*displs,MPI_Datatype sendtype,void* recvbuf,int
recvcount,MPI_Datatype recvtype,int root,MPI_Comm comm);
Parameters
sendbuf
is the address of the send buffer (choice, significant only at root) (IN)
sendcounts
integer array (of length groupsize) that contains the number of elements to
send to each task (significant only at root) (IN)
221
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
displs
integer array (of length groupsize). Entry I specifies the displacement relative to
sendbuf from which to send the outgoing data to task (significant only at root) (IN)
sendtype
is the datatype of the send buffer elements (handle, significant only at root)
(IN)
recvbuf
is the address of the receive buffer (choice) (OUT)
recvcount
is the number of elements in the receive buffer (integer) (IN)
recvtype
is the datatype of the receive buffer elements (handle) (IN)
root
is the rank of the sending task (integer) (IN)
comm
is the communicator (handle) (IN)
MPI_Group_compare
Compares the contents of two task groups.
#include <mpi.h>
int MPI_Group_compare(MPI_Group group1,MPI_Group group2,int *result);
Parameters
group1
is the first group (handle) (IN)
group2
is the second group (handle) (IN)
result
is the result (integer) (OUT)
MPI_Group_difference
Creates a new group that is the difference of two existing groups.
#include <mpi.h>
int MPI_Group_difference(MPI_Group group1,MPI_Group group2,MPI_Group
*newgroup);
Parameters
group1
is the first group (handle) (IN)
group2
is the second group (handle) (IN)
newgroup
is the difference group (handle) (OUT)
MPI_Group_excl
Creates a new group by excluding selected tasks of an existing group.
#include <mpi.h>
int MPI_Group_excl(MPI_Group group,int n,int *ranks,MPI_Group
*newgroup);
Parameters
group
is the group (handle) (IN)
n
is the number of elements in array ranks (integer) (IN)
ranks
is the array of integer ranks in group that is not to appear in newgroup (IN)
newgroup
is the new group derived from the above, preserving the order defined by group
(handle) (OUT)
MPI_Group_free
Marks a group for deallocation.
#include <mpi.h>
int MPI_Group_free(MPI_Group *group);
Parameters
222
∆.Σ. Βλάχος και Θ.Η. Σίµος
group
is the group (handle) (INOUT)
MPI_Group_incl
Creates a new group consisting of selected tasks from an existing group.
#include <mpi.h>
int MPI_Group_incl(MPI_Group group,int n,int *ranks,MPI_Group
*newgroup);
Parameters
group
is the group (handle) (IN)
n
is the number of elements in array ranks and the size of newgroup (integer) (IN)
ranks
is the ranks of tasks in group to appear in newgroup (array of integers) (IN)
newgroup
is the new group derived from above in the order defined by ranks (handle)
(OUT)
MPI_Group_intersection
Creates a new group that is the intersection of two existing groups.
#include <mpi.h>
int MPI_Group_intersection(MPI_Group group1,MPI_Group
group2,MPI_Group *newgroup);
Parameters
group1
is the first group (handle) (IN)
group2
is the second group (handle) (IN)
newgroup
is the intersection group (handle) (OUT)
MPI_Group_range_excl
Creates a new group by removing selected ranges of tasks from an existing group.
#include <mpi.h>
int MPI_Group_range_excl(MPI_Group group,int n,int
ranges[][3],MPI_Group *newgroup);
Parameters
group
is the group (handle) (IN)
n
is the number of triplets in array ranges (integer) (IN)
ranges
is an array of integer triplets of the form (first rank, last rank, stride) specifying the
ranks in group of tasks that are to be excluded from the output group newgroup. (IN)
newgroup
is the new group derived from above that preserves the order in group (handle)
(OUT)
MPI_Group_range_incl
Creates a new group consisting of selected ranges of tasks from an existing group.
#include <mpi.h>
int MPI_Group_range_incl(MPI_Group group,int n,int
ranges[][3],MPI_Group *newgroup);
Parameters
group
is the group (handle) (IN)
223
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
n
is the number of triplets in array ranges (integer) (IN)
ranges
is a one-dimensional array of integer triplets of the form (first_rank, last_rank,
stride) indicating ranks in group of tasks to be included in newgroup (IN)
newgroup
is the new group derived from above in the order defined by ranges (handle)
(OUT)
MPI_Group_rank
Returns the rank of the local task with respect to group.
#include <mpi.h>
int MPI_Group_rank(MPI_Group group,int *rank);
Parameters
group
is the group (handle) (IN)
rank
is an integer that specifies the rank of the calling task in group or
MPI_UNDEFINED if the task is not a member. (OUT)
MPI_Group_size
Returns the number of tasks in a group.
#include <mpi.h>
int MPI_Group_size(MPI_Group group,int *size);
Parameters
group
is the group (handle) (IN)
size
is the number of tasks in the group (integer) (OUT)
MPI_Group_translate_ranks
Converts task ranks of one group into ranks of another group.
#include <mpi.h>
int MPI_Group_translate_ranks(MPI_Group group1,int n,int
*ranks1,MPI_Group group2,int *ranks2);
Parameters
group1
is the first group (handle) (IN)
n
is an integer that specifies the number of ranks in ranks1 and ranks2 arrays (IN)
ranks1
is an array of zero or more valid ranks in group1 (IN)
group2
is the second group (handle) (IN)
ranks2
is an array of corresponding ranks in group2. If the
task of ranks1(i) is not a member of group2, ranks2(i)
returns MPI_UNDEFINED. (OUT)
MPI_Group_union
Creates a new group that is the union of two existing groups.
#include <mpi.h>
int MPI_Group_union(MPI_Group group1,MPI_Group group2,MPI_Group
*newgroup);
Parameters
group1
is the first group (handle) (IN)
group2
is the second group (handle) (IN)
224
∆.Σ. Βλάχος και Θ.Η. Σίµος
newgroup
is the union group (handle) (OUT)
MPI_Comm_compare
Compares the groups and context of two communicators.
#include <mpi.h>
int MPI_Comm_compare(MPI_Comm comm1,MPI_Comm comm2,int *result);
Parameters
comm1
is the first communicator (handle) (IN)
comm2
is the second communicator (handle) (IN)
result
is an integer specifying the result. The defined values are: MPI_IDENT,
MPI_CONGRUENT, MPI_SIMILAR, and MPI_UNEQUAL. (OUT)
MPI_Comm_create
Creates a new intracommunicator with a given group.
#include <mpi.h>
int MPI_Comm_create(MPI_Comm comm,MPI_Group group,MPI_Comm *newcomm);
Parameters
comm
is the communicator (handle) (IN)
group
is a subset of the group associated with comm (handle) (IN)
newcomm
is the new communicator (handle) (OUT)
MPI_Comm_dup
Creates a new communicator that is a duplicate of an existing communicator.
#include <mpi.h>
int MPI_Comm_dup(MPI_Comm comm,MPI_Comm *newcomm);
Parameters
comm
is the communicator (handle) (IN)
newcomm
is the copy of comm (handle) (OUT)
MPI_Comm_free
Marks a communicator for deallocation.
#include <mpi.h>
int MPI_Comm_free(MPI_Comm *comm);
Parameters
comm
is the communicator to be freed (handle) (INOUT)
MPI_Comm_group
Returns the group handle associated with a communicator.
#include <mpi.h>
int MPI_Comm_group(MPI_Comm comm,MPI_Group *group);
Parameters
comm
is the communicator (handle) (IN)
group
is the group corresponding to comm (handle) (OUT)
225
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Comm_rank
Returns the rank of the local task in the group associated with a communicator.
#include <mpi.h>
int MPI_Comm_rank(MPI_Comm comm,int *rank);
Parameters
comm
is the communicator (handle) (IN)
rank
is an integer specifying the rank of the calling task in group of comm (OUT)
MPI_Comm_remote_group
Returns the handle of the remote group of an intercommunicator.
#include <mpi.h>
int MPI_Comm_remote_group(MPI_Comm comm,MPI_group *group);
Parameters
comm
is the intercommunicator (handle) (IN)
group
is the remote group corresponding to comm. (OUT)
MPI_Comm_remote_size
Returns the size of the remote group of an intercommunicator.
#include <mpi.h>
int MPI_Comm_remote_size(MPI_Comm comm,int *size);
Parameters
comm
is the intercommunicator (handle) (IN)
size
is an integer specifying the number of tasks in the remote group of comm. (OUT)
MPI_Comm_size
Returns the size of the group associated with a communicator.
#include <mpi.h>
int MPI_Comm_size(MPI_Comm comm,int *size);
Parameters
comm
is the communicator (handle) (IN)
size
is an integer specifying the number of tasks in the group of comm (OUT)
MPI_Comm_split
Splits a communicator into multiple communicators based on color and key.
#include <mpi.h>
int MPI_Comm_split(MPI_Comm comm,int color,int key,MPI_Comm
*newcomm);
Parameters
comm
is the communicator (handle) (IN)
color
is an integer specifying control of subset assignment (IN)
key
is an integer specifying control of rank assignment (IN)
newcomm
is the new communicator (handle) (OUT)
MPI_Comm_test_inter
Returns the type of a communicator (intra or inter).
226
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Comm_test_inter(MPI_Comm comm,int *flag);
Parameters
comm
is the communicator (handle) (INOUT)
flag
communicator type (logical)
MPI_Intercomm_create
Creates an intercommunicator from two intracommunicators.
#include <mpi.h>
int MPI_Intercomm_create(MPI_Comm local_comm,int
local_leader,MPI_Comm peer_comm,int remote_leader,int tag,MPI_Comm
*newintercom);
Parameters
local_comm is the local intracommunicator (handle) (IN)
local_leader is an integer specifying the rank of local group leader in local_comm (IN)
peer_comm is the "peer" intracommunicator (significant only at the local_leader) (handle)
(IN)
remote_leader is the rank of the remote group leader in peer_comm (significant only at the
local_leader) (integer) (IN)
tag
"safe" tag (integer) (IN)
newintercom is the new intercommunicator (handle) (OUT)
MPI_Intercomm_merge
Creates an intracommunicator by merging the local and the remote groups of an
intercommunicator.
#include <mpi.h>
int MPI_Intercomm_merge(MPI_Comm intercomm,int high,MPI_Comm
*newintracom);
Parameters
intercomm
is the intercommunicator (handle) (IN)
high
(logical) (IN)
newintracomm
is the new intracommunicator (handle) (OUT)
MPI_Type_commit
Makes a datatype ready for use in communication.
#include <mpi.h>
int MPI_Type_commit(MPI_Datatype *datatype);
Parameters
datatype
is the datatype that is to be committed (handle) (INOUT)
MPI_Type_contiguous
Returns a new datatype that represents the concatenation of count instances of oldtype.
#include <mpi.h>
227
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
int MPI_Type_contiguous(int count,MPI_Datatype oldtype,MPI_Datatype
*newtype);
Parameters
count
is the replication count (non-negative integer) (IN)
oldtype
is the old datatype (handle) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Type_extent
Returns the extent of any defined datatype.
#include <mpi.h>
int MPI_Type_extent(MPI_Datatype datatype,MPI_Aint *size);
Parameters
datatype
is the datatype (handle) (IN)
size
is the datatype extent (integer) (OUT)
MPI_Type_free
Marks a datatype for deallocation.
#include <mpi.h>
int MPI_Type_free(MPI_Datatype *datatype);
Parameters
datatype
is the datatype to be freed (handle) (INOUT)
MPI_Type_hindexed
Returns a new datatype that represents count blocks. Each block is defined by an entry in
array_of_blocklengths and array_of_displacements. Displacements are expressed in bytes.
#include <mpi.h>
int MPI_Type_hindexed(int count,int *array_of_blocklengths,MPI_Aint
*array_of_displacements,MPI_Datatype oldtype,MPI_Datatype *newtype);
Parameters
count
is the number of blocks and the number of entries in
array_of_displacements and array_of_blocklengths (non-negative integer) (IN)
array_of_blocklengths is the number of instances of oldtype for each block (array of nonnegative integers)(IN)
array_of_displacements is a byte displacement for each block (array of integer) (IN)
oldtype
is the old datatype (handle) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Type_hvector
Returns a new datatype that represents equally-spaced blocks. The spacing between the
start of each block is given in bytes.
#include <mpi.h>
int MPI_Type_hvector(int count,int blocklength,MPI_Aint
stride,MPI_Datatype oldtype,MPI_Datatype *newtype);
Parameters
count
is the number of blocks (non-negative integer) (IN)
228
∆.Σ. Βλάχος και Θ.Η. Σίµος
blocklength
is the number of oldtype instances in each block (non-negative integer) (IN)
stride
is an integer specifying the number of bytes between start of each block. (IN)
oldtype
is the old datatype (handle) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Type_indexed
Returns a new datatype that represents count blocks. Each block is defined by an entry in
array_of_blocklengths and array_of_displacements. Displacements are expressed in units of
extent(oldtype).
#include <mpi.h>
int MPI_Type_indexed(int count,int *array_of_blocklengths,int
*array_of_displacements, MPI_Datatype oldtype,MPI_datatype *newtype);
Parameters
count
is the number of blocks and the number of entries in
array_of_displacements and array_of_blocklengths (non-negative integer) (IN)
array_of_blocklengths is the number of instances of oldtype in each block (array of nonnegative integers) (IN)
array_of_displacements is the displacement of each block in units of extent(oldtype) (array
of integer) (IN)
oldtype
is the old datatype (handle) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Type_lb
Returns the lower bound of a datatype.
#include <mpi.h>
int MPI_Type_lb(MPI_Datatype datatype,MPI_Aint *displacement);
Parameters
datatype
is the datatype (handle) (IN)
displacement is the displacement of lower bound from the origin in bytes (integer) (OUT)
MPI_Type_size
Returns the number of bytes represented by any defined datatype.
#include <mpi.h>
int MPI_Type_size(MPI_Datatype datatype,int *size);
Parameters
datatype
is the datatype (handle) (IN)
size
is the datatype size (integer) (OUT)
MPI_Type_struct
Returns a new datatype that represents count blocks. Each is defined by an entry in
array_of_blocklengths, array_of_displacements and array_of_types. Displacements are
expressed in bytes.
#include <mpi.h>
int MPI_Type_struct(int count,int *array_of_blocklengths,MPI_Aint
*array_of_displacements, MPI_Datatype *array_of_types, MPI_datatype
229
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
*newtype);
Parameters
count
is an integer specifying the number of blocks. It is also the number of
entries in arrays array_of_types, array_of_displacements and array_of_blocklengths. (IN)
array_of_blocklengths is the number of elements in each block (array of integer). That
is,array_of_blocklengths(i) specifies the number of instances of type array_of_types(i) in
block(i). (IN)
array_of_displacements is the byte displacement of each block (array of integer) (IN)
array_of_types
is the datatype comprising each block. That is, block(i) is made of a
concatenation of type array_of_types(i). (array of handles to datatype objects) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Type_ub
Returns the upper bound of a datatype.
#include <mpi.h>
int MPI_Type_ub(MPI_Datatype datatype,MPI_Aint *displacement);
Parameters
datatype
is the datatype (handle) (IN)
displacement is the displacement of the upper bound from the origin, in bytes (integer) (OUT)
MPI_Type_vector
Returns a new datatype that represents equally spaced blocks. The spacing between the start
of each block is given in units of extent (oldtype).
#include <mpi.h>
int MPI_Type_vector(int count,int blocklength,int stride,MPI_Datatype
oldtype,MPI_Datatype *newtype);
Parameters
count
is the number of blocks (non-negative integer) (IN)
blocklength
is the number of oldtype instances in each block (non-negative integer) (IN)
stride
is the number of units between the start of each block (integer) (IN)
oldtype
is the old datatype (handle) (IN)
newtype
is the new datatype (handle) (OUT)
MPI_Cart_coords
Translates task rank in a communicator into cartesian task coordinates.
#include <mpi.h>
MPI_Cart_coords(MPI_Comm comm,int rank,int maxdims,int *coords);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
rank
is the rank of a task within group comm (integer) (IN)
maxdims
is the length of array coords in the calling program (integer) (IN)
coords
is an integer array specifying the cartesian coordinates of a task. (OUT)
MPI_Cart_create
Creates a communicator containing topology information.
#include <mpi.h>
230
∆.Σ. Βλάχος και Θ.Η. Σίµος
int MPI_Cart_create(MPI_Comm comm_old,int ndims,int *dims,int
*periods,int reorder,MPI_Comm *comm_cart);
Parameters
comm_old
is the input communicator (handle) (IN)
ndims
is the number of cartesian dimensions in the grid (integer) (IN)
dims
is an integer array of size ndims specifying the number of tasks in each dimension
(IN)
periods
is a logical array of size ndims specifying if the gridis periodic or not in each
dimension (IN)
reorder
if true, ranking may be reordered. If false, then rank in comm_cart must be the
same as in comm_old. (logical) (IN)
comm_cart is a communicator with new cartesian topology (handle) (OUT)
MPI_Cart_get
Retrieves cartesian topology information from a communicator.
#include <mpi.h>
MPI_Cart_get(MPI_Comm comm,int maxdims,int *dims,int *periods,int
*coords);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
maxdims
is the length of dims, periods, and coords in the calling program (integer)
(IN)
dims
is the number of tasks for each Cartesian dimension (array of integer) (OUT)
periods
is a logical array specifying if each Cartesian dimension is periodic or not.
(OUT)
coords
is the coordinates of the calling task in the cartesian structure (array of
integer) (OUT)
MPI_Cart_map
Computes placement of tasks on the physical machine.
#include <mpi.h>
MPI_Cart_map(MPI_Comm comm,int ndims,int *dims,int *periods,int
*newrank);
Parameters
comm
is the input communicator (handle) (IN)
ndims
is the number of dimensions of the cartesian structure (integer) (IN)
dims
is an integer array of size ndims specifying the number of tasks in each coordinate
direction (IN)
periods
is a logical array of size ndims specifying the periodicity in each coordinate
direction (IN)
newrank
is the reordered rank or MPI_UNDEFINED if the calling task does not belong to
the grid (integer) (OUT)
MPI_Cart_rank
Translates task coordinates into a task rank.
231
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
#include <mpi.h>
MPI_Cart_rank(MPI_Comm comm,int *coords,int *rank);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
coords
is an integer array of size ndims specifying the cartesian coordinates of a task (IN)
rank
is an integer specifying the rank of specified task (OUT)
MPI_Cart_shift
Returns shifted source and destination ranks for a task.
#include <mpi.h>
MPI_Cart_shift(MPI_Comm comm,int direction,int disp,int
*rank_source,int *rank_dest);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
direction
is the coordinate dimension of shift (integer) (IN)
disp
is the displacement (> 0 = upward shift, < 0 =downward shift) (integer) (IN)
rank_source
is the rank of the source task (integer) (OUT)
rank_dest
is the rank of the destination task (integer) (OUT)
MPI_Cart_sub
Partitions a cartesian communicator into lower-dimensional subgroups.
#include <mpi.h>
MPI_Cart_sub(MPI_Comm comm,int *remain_dims,MPI_Comm *newcomm);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
remain_dims the ith entry of remain_dims specifies whether the ith dimension is kept in the
subgrid or is dropped. (logical vector) (IN)
newcomm
is the communicator containing the subgrid that includes the calling task
(handle) (OUT)
MPI_Cartdim_get
Retrieves the number of cartesian dimensions from a communicator.
#include <mpi.h>
MPI_Cartdim_get(MPI_Comm comm,int *ndims);
Parameters
comm
is a communicator with cartesian topology (handle) (IN)
ndims
is an integer specifying the number of dimensions of the cartesian topology (OUT)
MPI_Dims_create
Defines a cartesian grid to balance tasks.
#include <mpi.h>
MPI_Dims_create(int nnodes,int ndims,int *dims);
Parameters
nnodes
is an integer specifying the number of nodes in a grid (IN)
ndims
is an integer specifying the number of Cartesian dimensions (IN)
232
∆.Σ. Βλάχος και Θ.Η. Σίµος
dims
is an integer array of size ndims that specifies the number of nodes in each
dimension. (INOUT)
MPI_Graph_create
Creates a new communicator containing graph topology information.
#include <mpi.h>
MPI_Graph_create(MPI_Comm comm_old,int nnodes, int *index,int
*edges,int reorder, MPI_Comm *comm_graph);
Parameters
comm_old
is the input communicator (handle) (IN)
nnodes
is an integer specifying the number of nodes in the graph (IN)
index
is an array of integers describing node degrees (IN)
edges
is an array of integers describing graph edges (IN)
reorder
if true, ranking may be reordered (logical) (IN)
comm_graph
is the communicator with the graph topology added (handle) (OUT)
MPI_Graph_get
Retrieves graph topology information from a communicator.
#include <mpi.h>
MPI_Graph_get(MPI_Comm comm,int maxindex,int maxedges, int *index,int
*edges);
Parameters
comm
is a communicator with graph topology (handle) (IN)
maxindex
is an integer specifying the length of index in the calling program (IN)
maxedges
is an integer specifying the length of edges in the calling program (IN)
index
is an array of integers containing node degrees (OUT)
edges
is an array of integers containing node neighbors (OUT)
MPI_Graph_map
Computes placement of tasks on the physical machine.
#include <mpi.h>
MPI_Graph_map(MPI_Comm comm,int nnodes,int *index,int *edges,int
*newrank);
Parameters
comm
is the input communicator (handle) (IN)
nnodes
is the number of graph nodes (integer) (IN)
index
is an integer array specifying node degrees (IN)
edges
is an integer array specifying node adjacency (IN)
newrank
is the reordered rank,or MPI_Undefined if the calling task does not belong to the
graph (integer) (OUT)
MPI_Graph_neighbors
Returns the neighbors of the given task.
#include <mpi.h>
MPI_Graph_neighbors(MPI_Comm comm,int rank,int maxneighbors,int
233
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
*neighbors);
Parameters
comm
is a communicator with graph topology (handle) (IN)
rank
is the rank of a task within group of comm. (integer) (IN)
maxneighbors
is the size of array neighbors (integer) (IN)
neighbors
is the ranks of tasks that are neighbors of the specified task (array of integer)
(OUT)
MPI_Graph_neighbors_count
Returns the number of neighbors of the given task.
#include <mpi.h>
MPI_Graph_neighbors_count(MPI_Comm comm,int rank,int *neighbors);
Parameters
comm
is a communicator with graph topology (handle) (IN)
rank
is the rank of a task within comm (integer) (IN)
neighbors is the number of neighbors of the specified task (integer) (OUT)
MPI_Graphdims_get
Retrieves graph topology information from a communicator.
#include <mpi.h>
MPI_Graphdims_get(MPI_Comm comm,int *nnodes,int *nedges);
Parameters
comm
is a communicator with graph topology (handle) (IN)
nnodes
is an integer specifying the number of nodes in the graph. The number of nodes
and the number of tasks in the group are equal. (OUT)
nedges
is an integer specifying the number of edges in the graph. (OUT)
MPI_Topo_test
Returns the type of virtual topology associated with a communicator.
#include <mpi.h>
MPI_Topo_test(MPI_Comm comm,int *status);
Parameters
comm
is the communicator (handle) (IN)
status
is the topology type of communicator comm (integer) (OUT)
MPI_Address
Returns the address of a variable in memory.
#include <mpi.h>
int MPI_Address(void* location,MPI_Aint *address);
Parameters
location
is the location in caller memory (choice) (IN)
address
is the address of location (integer) (OUT)
MPI_Attr_delete
Removes an attribute value from a communicator.
234
∆.Σ. Βλάχος και Θ.Η. Σίµος
#include <mpi.h>
int MPI_Attr_delete(MPI_Comm comm,int keyval);
Parameters
comm
is the communicator that the attribute is attached (handle) (IN)
keyval
is the key value of the deleted attribute (integer) (IN)
MPI_Attr_get
Retrieves an attribute value from a communicator.
#include <mpi.h>
int MPI_Attr_get(MPI_Comm comm,int keyval,void *attribute_val,int
*flag);
Parameters
comm
is the communicator to which attribute is attached
(handle) (IN)
keyval
is the key value (integer) (IN)
attribute_val is the attribute value unless flag = false (OUT)
flag
is true if an attribute value was extracted and false if no attribute is associated with
the key. (OUT)
MPI_Attr_put
Stores an attribute value in a communicator.
#include <mpi.h>
int MPI_Attr_put(MPI_Comm comm,int keyval,void* attribute_val);
Parameters
comm
is the communicator to which attribute will be attached (handle) (IN)
keyval
is the key value as returned by MPI_KEYVAL_CREATE (integer) (IN)
attribute_val is the attribute value (IN)
MPI_Keyval_create
Generates a new communicator attribute key.
#include <mpi.h>
int MPI_Keyval_create(MPI_Copy_function *copy_fn, MPI_Delete_function
*delete_fn,int *keyval, void* extra_state);
Parameters
copy_fn
is the copy callback function for keyval (IN)
delete_fn is the delete callback function for keyval (IN)
keyval
is an integer specifying the key value for future access (OUT)
extra_state is the extra state for callback functions (IN)
MPI_Keyval_free
Marks a communicator attribute key for deallocation.
#include <mpi.h>
int MPI_Keyval_free(int *keyval);
235
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Parameters
keyval
attribute key (integer) (INOUT)
MPI_Pack
Packs the message in the specified send buffer into the specified buffer space.
#include <mpi.h>
int MPI_Pack(void* inbuf,int incount,MPI_Datatype datatype, void
*outbuf,int outsize,int *position,MPI_Comm comm);
Parameters
inbuf
is the input buffer start (choice) (IN)
incount
is an integer specifying the number of input data items (IN)
datatype
is the datatype of each input data item (handle) (IN)
outbuf
is the output buffer start (choice) (OUT)
outsize
is an integer specifying the output buffer size in bytes (OUT)
position
is the current position in the output buffer counted in bytes (integer) (INOUT)
comm
is the communicator for sending the packed message (handle) (IN)
MPI_Pack_size
Returns the number of bytes required to hold the data.
#include <mpi.h>
int MPI_Pack_size(int incount,MPI_Datatype datatype,MPI_Comm comm,
int *size);
Parameters
incount
is an integer specifying the count argument to a packing call (IN)
datatype
is the datatype argument to a packing call (handle) (IN)
comm
is the communicator to a packing call (handle) (IN)
size
is the size of packed message in bytes (integer) (OUT)
MPI_Pcontrol
Provides profiler control.
#include <mpi.h>
int MPI_Pcontrol(const int level, ...);
Parameters
level
is the profiling level (IN)
MPI_Unpack
Unpacks the message into the specified receive buffer from the specified packed buffer.
#include <mpi.h>
int MPI_Unpack(void* inbuf,int insize,int *position, void *outbuf,int
outcount,MPI_Datatype datatype, MPI_Comm comm);
Parameters
inbuf
is the input buffer start (choice) (IN)
insize
is an integer specifying the size of input buffer in bytes (IN)
position
is an integer specifying the current packed buffer offset in bytes (INOUT)
outbuf
is the output buffer start (choice) (OUT)
outcount
is an integer specifying the number of instances of datatype to be unpacked (IN)
236
∆.Σ. Βλάχος και Θ.Η. Σίµος
datatype
comm
is the datatype of each output data item (handle) (IN)
is the communicator for the packed message (handle) (IN)
237
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
238
∆.Σ. Βλάχος και Θ.Η. Σίµος
Παράρτηµα ΙΙΙ
Σύντοµος οδηγός του MPI
What is MPI?
An Interface Specification:
•
•
•
•
M P I = Message Passing Interface
MPI is a specification for the developers and users of message passing libraries. By itself, it
is NOT a library - but rather the specification of what such a library should be.
Simply stated, the goal of the Message Passing Interface is to provide a widely used standard
for writing message passing programs. The interface attempts to be
o practical
o portable
o efficient
o flexible
Interface specifications have been defined for C and Fortran programs.
History:
•
•
•
•
•
•
•
MPI resulted from the efforts of numerous individuals and groups over the course of a 2 year
period between 1992 and 1994. Some history:
1980s - early 1990s: Distributed memory, parallel computing develops, as do a number of
incompatible software tools for writing such programs - usually with tradeoffs between
portability, performance, functionality and price. Recognition of the need for a standard arose.
April, 1992: Workshop on Standards for Message Passing in a Distributed Memory
Environment, sponsored by the Center for Research on Parallel Computing, Williamsburg,
Virginia. The basic features essential to a standard message passing interface were discussed,
and a working group established to continue the standardization process. Preliminary draft
proposal developed subsequently.
November 1992: - Working group meets in Minneapolis. MPI draft proposal (MPI1) from
ORNL presented. Group adopts procedures and organization to form the MPI Forum. MPIF
eventually comprised of about 175 individuals from 40 organizations including parallel
computer vendors, software writers, academia and application scientists.
November 1993: Supercomputing 93 conference - draft MPI standard presented.
Final version of draft released in May, 1994 - available on the WWW at:
http://www.mcs.anl.gov/Projects/mpi/standard.html
MPI-2 picked up where the first MPI specification left off, and addresses topics which go
beyond the first MPI specification. MPI-2 is briefly covered later.
239
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Reasons for Using MPI:
•
•
•
•
•
Standardization - MPI is the only message passing library which can be considered a
standard. It is supported on virtually all HPC platforms. Practically, it has replaced all
previous message passing libraries.
Portability - There is no need to modify your source code when you port your application to a
different platform that supports (and is compliant with) the MPI standard.
Performance Opportunities - Vendor implementations should be able to exploit native
hardware features to optimize performance. For more information about MPI performance see
the MPI Performance Topics tutorial.
Functionality - Over 115 routines are defined.
Availability - A variety of implementations are available, both vendor and public domain.
Programming Model:
•
•
•
•
MPI lends itself to most (if not all) distributed memory parallel programming models. In
addition, MPI is commonly used to implement (behind the scenes) some shared memory
models, such as Data Parallel, on distributed memory architectures.
Hardware platforms:
o Distributed Memory: Originally, MPI was targeted for distributed memory systems.
o Shared Memory: As shared memory systems became more popular, particularly SMP
/ NUMA architectures, MPI implementations for these platforms appeared.
o Hybrid: MPI is now used on just about any common parallel architecture including
massively parallel machines, SMP clusters, workstation clusters and heterogeneous
networks.
All parallelism is explicit: the programmer is responsible for correctly identifying parallelism
and implementing parallel algorithms using MPI constructs.
The number of tasks dedicated to run a parallel program is static. New tasks can not be
dynamically spawned during run time. (MPI-2 addresses this issue).
Getting Started
Header File:
•
Required for all programs/routines which make MPI library calls.
C include file
#include "mpi.h"
Format of MPI Calls:
C Binding
240
∆.Σ. Βλάχος και Θ.Η. Σίµος
Format:
Example:
Error
code:
•
rc = MPI_Xxxxx(parameter, ... )
rc =
MPI_Bsend(&buf,count,type,dest,tag,comm..)
Returned as "rc". MPI_SUCCESS if successful
C names are case sensitive; Fortran names are not.
General MPI Program Structure:
Communicators and Groups:
•
•
MPI uses objects called communicators and groups to define which collection of processes
may communicate with each other. Most MPI routines require you to specify a communicator
as an argument.
Communicators and groups will be covered in more detail later. For now, simply use
MPI_COMM_WORLD whenever a communicator is required - it is the predefined
communicator that includes all of your MPI processes.
241
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Rank:
•
•
Within a communicator, every process has its own unique, integer identifier assigned by the
system when the process initializes. A rank is sometimes also called a "process ID". Ranks are
contiguous and begin at zero.
Used by the programmer to specify the source and destination of messages. Often used
conditionally by the application to control program execution (if rank=0 do this / if rank=1 do
that).
Environment Management Routines
MPI environment management routines are used for an assortment of purposes, such as initializing and
terminating the MPI environment, querying the environment and identity, etc. Most of the commonly
used ones are described below.
MPI_Init
Initializes the MPI execution environment. This function must be called in every MPI
program, must be called before any other MPI functions and must be called only once in an
MPI program. For C programs, MPI_Init may be used to pass the command line arguments to
all processes, although this is not required by the standard and is implementation dependent.
MPI_Init (&argc,&argv)
MPI_Comm_size
Determines the number of processes in the group associated with a communicator. Generally
used within the communicator MPI_COMM_WORLD to determine the number of processes
being used by your application.
MPI_Comm_size (comm,&size)
MPI_Comm_rank
242
∆.Σ. Βλάχος και Θ.Η. Σίµος
Determines the rank of the calling process within the communicator. Initially, each process
will be assigned a unique integer rank between 0 and number of processors - 1 within the
communicator MPI_COMM_WORLD. This rank is often referred to as a task ID. If a process
becomes associated with other communicators, it will have a unique rank within each of these
as well.
MPI_Comm_rank (comm,&rank)
MPI_Abort
Terminates all MPI processes associated with the communicator. In most MPI
implementations it terminates ALL processes regardless of the communicator specified.
MPI_Abort (comm,errorcode)
MPI_Get_processor_name
Returns the processor name. Also returns the length of the name. The buffer for "name" must
be at least MPI_MAX_PROCESSOR_NAME characters in size. What is returned into "name"
is implementation dependent - may not be the same as the output of the "hostname" or "host"
shell commands.
MPI_Get_processor_name (&name,&resultlength)
MPI_Initialized
Indicates whether MPI_Init has been called - returns flag as either logical true (1) or false(0).
MPI requires that MPI_Init be called once and only once by each process. This may pose a
problem for modules that want to use MPI and are prepared to call MPI_Init if necessary.
MPI_Initialized solves this problem.
MPI_Initialized (&flag)
MPI_Wtime
Returns an elapsed wall clock time in seconds (double precision) on the calling processor.
MPI_Wtime ()
MPI_Wtick
Returns the resolution in seconds (double precision) of MPI_Wtime.
MPI_Wtick ()
MPI_Finalize
Terminates the MPI execution environment. This function should be the last MPI routine
called in every MPI program - no other MPI routines may be called after it.
243
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Finalize ()
Examples: Environment Management Routines
C Language - Environment Management Routines Example
#include "mpi.h"
#include <stdio.h>
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, rc;
rc = MPI_Init(&argc,&argv);
if (rc != MPI_SUCCESS) {
printf ("Error starting MPI program. Terminating.\n");
MPI_Abort(MPI_COMM_WORLD, rc);
}
MPI_Comm_size(MPI_COMM_WORLD,&numtasks);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
printf ("Number of tasks= %d My rank= %d\n", numtasks,rank);
/*******
do some work *******/
MPI_Finalize();
}
Point to Point Communication Routines
General Concepts
Types of Point-to-Point Operations:
•
MPI point-to-point operations typically involve message passing between two, and only two,
different MPI tasks. One task is performing a send operation and the other task is performing a
matching receive operation.
244
∆.Σ. Βλάχος και Θ.Η. Σίµος
•
•
•
There are different types of send and receive routines used for different purposes. For
example:
o Synchronous send
o Blocking send / blocking receive
o Non-blocking send / non-blocking receive
o Buffered send
o Combined send/receive
o "Ready" send
Any type of send routine can be paired with any type of receive routine.
MPI also provides several routines associated with send - receive operations, such as those
used to wait for a message's arrival or probe to find out if a message has arrived.
Buffering:
•
•
•
•
In a perfect world, every send operation would be perfectly synchronized with its matching
receive. This is rarely the case. Somehow or other, the MPI implementation must be able to
deal with storing data when the two tasks are out of sync.
Consider the following two cases:
o A send operation occurs 5 seconds before the receive is ready - where is the message
while the receive is pending?
o Multiple sends arrive at the same receiving task which can only accept one send at a
time - what happens to the messages that are "backing up"?
The MPI implementation (not the MPI standard) decides what happens to data in these types
of cases. Typically, a system buffer area is reserved to hold data in transit. For example:
System buffer space is:
o Opaque to the programmer and managed entirely by the MPI library
o A finite resource that can be easy to exhaust
245
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Often mysterious and not well documented
Able to exist on the sending side, the receiving side, or both
Something that may improve program performance because it allows send - receive
operations to be asynchronous.
User managed address space (i.e. your program variables) is called the application buffer.
MPI also provides for a user managed send buffer.
o
o
o
•
Blocking vs. Non-blocking:
•
•
•
Most of the MPI point-to-point routines can be used in either blocking or non-blocking mode.
Blocking:
o A blocking send routine will only "return" after it safe to modify the application
buffer (your send data) for reuse. Safe means that modifications will not affect the
data intended for the receive task.
o A blocking send can be synchronous which means there is handshaking occurring
with the receive task to confirm a safe send.
o A blocking send can be asynchronous if a system buffer is used to hold the data for
eventual delivery to the receive.
o A blocking receive only "returns" after the data has arrived and is ready for use by
the program.
Non-blocking:
o Non-blocking send and receive routines behave similarly - they will return almost
immediately. They do not wait for any communication events to complete, such as
message copying from user memory to system buffer space or the actual arrival of
message.
o Non-blocking operations simply "request" the MPI library to perform the operation
when it is able. The user can not predict when that will happen.
o It is unsafe to modify the application buffer (your variable space) until you know for
a fact the requested non-blocking operation was actually performed by the library.
There are "wait" routines used to do this.
o Non-blocking communications are primarily used to overlap computation with
communication and exploit possible performance gains.
Order and Fairness:
•
•
Order:
o MPI guarantees that messages will not overtake each other.
o If a sender sends two messages (Message 1 and Message 2) in succession, to the
same destination, and both match the same receive. The receive operation will
receive Message 1 before Message 2.
o If a receiver posts two receives (Receive 1 and Receive 2), in succession, and both
are looking for the same message, Receive 1 will receive the message before Receive
2.
o Order rules do not apply is there are multiple threads participating in the
communication operations.
Fairness:
o MPI does not guarentee fairness - it's up to the programmer to prevent "operation
starvation".
246
∆.Σ. Βλάχος και Θ.Η. Σίµος
o
Example: task 0 sends a message to task 2. However, task 1 sends a competing
message that matches task 2's receive. Only one of the sends will complete.
Point to Point Communication Routines
MPI Message Passing Routine Arguments
MPI point-to-point communication routines generally have an argument list that takes
one of the following formats:
Blocking
sends
MPI_Send(buffer,count,type,dest,tag,comm)
Nonblocking
sends
MPI_Isend(buffer,count,type,dest,tag,comm,request)
Blocking
receive
MPI_Recv(buffer,count,type,source,tag,comm,status)
Nonblocking
receive
MPI_Irecv(buffer,count,type,source,tag,comm,request)
Buffer
Program (application) address space that references the data that is to be sent or or received. In
most cases, this is simply the variable name that is be sent/received. For C programs, this
argument is passed by reference and usually must be prepended with an ampersand: &var1
Data Count
Indicates the number of data elements of a particular type to be sent.
Data Type
For reasons of portability, MPI predefines its elementary data types. The table below lists
those required by the standard.
C Data Types
MPI_CHAR
signed char
MPI_SHORT
signed short int
MPI_INT
signed int
247
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_LONG
signed long int
MPI_UNSIGNED_CHAR
unsigned char
MPI_UNSIGNED_SHORT
unsigned short int
MPI_UNSIGNED
unsigned int
MPI_UNSIGNED_LONG
unsigned long int
MPI_FLOAT
float
MPI_DOUBLE
double
MPI_LONG_DOUBLE
long double
MPI_BYTE
8 binary digits
MPI_PACKED
data packed or unpacked with
MPI_Pack()/ MPI_Unpack
Notes:
•
•
•
•
Programmers may also create their own data types (see Derived Data Types).
MPI_BYTE and MPI_PACKED do not correspond to standard C or Fortran types.
The MPI standard includes the following optional data types:
o C: MPI_LONG_LONG_INT
o Fortran: MPI_INTEGER1, MPI_INTEGER2, MPI_INTEGER4,
MPI_REAL2, MPI_REAL4, MPI_REAL8
Some implementations may include additional elementary data types
(MPI_LOGICAL2, MPI_COMPLEX32, etc.)
Destination
An argument to send routines that indicates the process where a message should be delivered.
Specified as the rank of the receiving process.
Source
An argument to receive routines that indicates the originating process of the message.
Specified as the rank of the sending process. This may be set to the wild card
MPI_ANY_SOURCE to receive a message from any task.
Tag
Arbitrary non-negative integer assigned by the programmer to uniquely identify a message.
Send and receive operations should match message tags. For a receive operations, the wild
card MPI_ANY_TAG can be used to receive any message regardless of its tag. The MPI
standard guarantees that integers 0-32767 can be used as tags, but most implementations allow
a much larger range than this.
Communicator
248
∆.Σ. Βλάχος και Θ.Η. Σίµος
Indicates the communication context, or set of processes for which the source or destination
fields are valid. Unless the programmer is explicitly creating new communicators, the
predefined communicator MPI_COMM_WORLD is usually used.
Status
For a receive operation, indicates the source of the message and the tag of the message. In C,
this argument is a pointer to a predefined structure MPI_Status (ex. stat.MPI_SOURCE
stat.MPI_TAG). In Fortran, it is an integer array of size MPI_STATUS_SIZE (ex.
stat(MPI_SOURCE) stat(MPI_TAG)). Additionally, the actual number of bytes received are
obtainable from Status via the MPI_Get_count routine.
Request
Used by non-blocking send and receive operations. Since non-blocking operations may return
before the requested system buffer space is obtained, the system issues a unique "request
number". The programmer uses this system assigned "handle" later (in a WAIT type routine)
to determine completion of the non-blocking operation. In C, this argument is a pointer to a
predefined structure MPI_Request. In Fortran, it is an integer.
Point to Point Communication Routines
Blocking Message Passing Routines
The more commonly used MPI blocking message passing routines are described below.
MPI_Send
Basic blocking send operation. Routine returns only after the application buffer in the sending
task is free for reuse. Note that this routine may be implemented differently on different
systems. The MPI standard permits the use of a system buffer but does not require it. Some
implementations may actually use a synchronous send (discussed below) to implement the
basic blocking send.
MPI_Send (&buf,count,datatype,dest,tag,comm)
MPI_Recv
Receive a message and block until the requested data is available in the application buffer in
the receiving task.
MPI_Recv (&buf,count,datatype,source,tag,comm,&status)
MPI_Ssend
Synchronous blocking send: Send a message and block until the application buffer in the
sending task is free for reuse and the destination process has started to receive the message.
MPI_Ssend (&buf,count,datatype,dest,tag,comm,ierr)
249
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Bsend
Buffered blocking send: permits the programmer to allocate the required amount of buffer
space into which data can be copied until it is delivered. Insulates against the problems
associated with insufficient system buffer space. Routine returns after the data has been copied
from application buffer space to the allocated send buffer. Must be used with the
MPI_Buffer_attach routine.
MPI_Bsend (&buf,count,datatype,dest,tag,comm)
MPI_Buffer_attach
MPI_Buffer_detach
Used by programmer to allocate/deallocate message buffer space to be used by the
MPI_Bsend routine. The size argument is specified in actual data bytes - not a count of data
elements. Only one buffer can be attached to a process at a time. Note that the IBM
implementation uses MPI_BSEND_OVERHEAD bytes of the allocated buffer for overhead.
MPI_Buffer_attach (&buffer,size)
MPI_Buffer_detach (&buffer,size)
MPI_Rsend
Blocking ready send. Should only be used if the programmer is certain that the matching
receive has already been posted.
MPI_Rsend (&buf,count,datatype,dest,tag,comm)
MPI_Sendrecv
Send a message and post a receive before blocking. Will block until the sending application
buffer is free for reuse and until the receiving application buffer contains the received
message.
MPI_Sendrecv (&sendbuf,sendcount,sendtype,dest,sendtag,
...... &recvbuf,recvcount,recvtype,source,recvtag,
...... comm,&status)
MPI_Wait
MPI_Waitany
MPI_Waitall
MPI_Waitsome
MPI_Wait blocks until a specified non-blocking send or receive operation has completed. For
multiple non-blocking operations, the programmer can specify any, all or some completions.
MPI_Wait (&request,&status)
MPI_Waitany (count,&array_of_requests,&index,&status)
250
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Waitall (count,&array_of_requests,&array_of_statuses)
MPI_Waitsome (incount,&array_of_requests,&outcount,
...... &array_of_offsets, &array_of_statuses)
MPI_Probe
Performs a blocking test for a message. The "wildcards" MPI_ANY_SOURCE and
MPI_ANY_TAG may be used to test for a message from any source or with any tag. For the C
routine, the actual source and tag will be returned in the status structure as
status.MPI_SOURCE and status.MPI_TAG. For the Fortran routine, they will be returned in
the integer array status(MPI_SOURCE) and status(MPI_TAG).
MPI_Probe (source,tag,comm,&status)
Examples: Blocking Message Passing Routines
Task 0 pings task 1 and awaits return ping
C Language - Blocking Message Passing Routines Example
#include "mpi.h"
#include <stdio.h>
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, dest, source, rc, count, tag=1;
char inmsg, outmsg='x';
MPI_Status Stat;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0) {
dest = 1;
source = 1;
rc = MPI_Send(&outmsg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);
rc = MPI_Recv(&inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD,
&Stat);
}
251
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
else if (rank == 1) {
dest = 0;
source = 0;
rc = MPI_Recv(&inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD,
&Stat);
rc = MPI_Send(&outmsg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);
}
rc = MPI_Get_count(&Stat, MPI_CHAR, &count);
printf("Task %d: Received %d char(s) from task %d with tag %d \n",
rank, count, Stat.MPI_SOURCE, Stat.MPI_TAG);
MPI_Finalize();
}
Point to Point Communication Routines
Non-Blocking Message Passing Routines
The more commonly used MPI non-blocking message passing routines are described below.
MPI_Isend
Identifies an area in memory to serve as a send buffer. Processing continues immediately
without waiting for the message to be copied out from the application buffer. A
communication request handle is returned for handling the pending message status. The
program should not modify the application buffer until subsequent calls to MPI_Wait or
MPI_Test indicates that the non-blocking send has completed.
MPI_Isend (&buf,count,datatype,dest,tag,comm,&request)
MPI_Irecv
Identifies an area in memory to serve as a receive buffer. Processing continues immediately
without actually waiting for the message to be received and copied into the the application
buffer. A communication request handle is returned for handling the pending message status.
The program must use calls to MPI_Wait or MPI_Test to determine when the non-blocking
receive operation completes and the requested message is available in the application buffer.
MPI_Irecv (&buf,count,datatype,source,tag,comm,&request)
MPI_Issend
Non-blocking synchronous send. Similar to MPI_Isend(), except MPI_Wait() or MPI_Test()
indicates when the destination process has received the message.
252
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Issend (&buf,count,datatype,dest,tag,comm,&request)
MPI_Ibsend
Non-blocking buffered send. Similar to MPI_Bsend() except MPI_Wait() or MPI_Test()
indicates when the destination process has received the message. Must be used with the
MPI_Buffer_attach routine.
MPI_Ibsend (&buf,count,datatype,dest,tag,comm,&request)
MPI_Irsend
Non-blocking ready send. Similar to MPI_Rsend() except MPI_Wait() or MPI_Test()
indicates when the destination process has received the message. Should only be used if the
programmer is certain that the matching receive has already been posted.
MPI_Irsend (&buf,count,datatype,dest,tag,comm,&request)
MPI_Test
MPI_Testany
MPI_Testall
MPI_Testsome
MPI_Test checks the status of a specified non-blocking send or receive operation. The "flag"
parameter is returned logical true (1) if the operation has completed, and logical false (0) if
not. For multiple non-blocking operations, the programmer can specify any, all or some
completions.
MPI_Test (&request,&flag,&status)
MPI_Testany (count,&array_of_requests,&index,&flag,&status)
MPI_Testall (count,&array_of_requests,&flag,&array_of_statuses)
MPI_Testsome (incount,&array_of_requests,&outcount,
...... &array_of_offsets, &array_of_statuses)
MPI_Iprobe
Performs a non-blocking test for a message. The "wildcards" MPI_ANY_SOURCE and
MPI_ANY_TAG may be used to test for a message from any source or with any tag. The
integer "flag" parameter is returned logical true (1) if a message has arrived, and logical false
(0) if not. For the C routine, the actual source and tag will be returned in the status structure as
status.MPI_SOURCE and status.MPI_TAG. For the Fortran routine, they will be returned in
the integer array status(MPI_SOURCE) and status(MPI_TAG).
MPI_Iprobe (source,tag,comm,&flag,&status)
253
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Examples: Non-Blocking Message Passing
Routines
Nearest neighbor exchange in ring topology
C Language - Non-Blocking Message Passing Routines
Example
#include "mpi.h"
#include <stdio.h>
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, next, prev, buf[2], tag1=1, tag2=2;
MPI_Request reqs[4];
MPI_Status stats[4];
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
prev = rank-1;
next = rank+1;
if (rank == 0) prev = numtasks - 1;
if (rank == (numtasks - 1)) next = 0;
MPI_Irecv(&buf[0], 1, MPI_INT, prev, tag1, MPI_COMM_WORLD,
&reqs[0]);
MPI_Irecv(&buf[1], 1, MPI_INT, next, tag2, MPI_COMM_WORLD,
&reqs[1]);
MPI_Isend(&rank, 1, MPI_INT, prev, tag2, MPI_COMM_WORLD, &reqs[2]);
MPI_Isend(&rank, 1, MPI_INT, next, tag1, MPI_COMM_WORLD, &reqs[3]);
{
do some work
}
MPI_Waitall(4, reqs, stats);
MPI_Finalize();
}
Collective Communication Routines
254
∆.Σ. Βλάχος και Θ.Η. Σίµος
All or None:
•
•
Collective communication must involve all processes in the scope of a communicator. All
processes are by default, members in the communicator MPI_COMM_WORLD.
It is the programmer's responsibility to insure that all processes within a communicator
participate in any collective operations.
Types of Collective Operations:
•
•
•
Synchronization - processes wait until all members of the group have reached the
synchronization point.
Data Movement - broadcast, scatter/gather, all to all.
Collective Computation (reductions) - one member of the group collects data from the other
members and performs an operation (min, max, add, multiply, etc.) on that data.
Programming Considerations and Restrictions:
•
•
•
•
Collective operations are blocking.
Collective communication routines do not take message tag arguments.
Collective operations within subsets of processes are accomplished by first partitioning the
subsets into a new groups and then attaching the new groups to new communicators
(discussed in the Group and Communicator Management Routines section).
Can only be used with MPI predefined datatypes - not with MPI Derived Data Types.
Collective Communication Routines
MPI_Barrier
Creates a barrier synchronization in a group. Each task, when reaching the MPI_Barrier call,
blocks until all tasks in the group reach the same MPI_Barrier call.
MPI_Barrier (comm)
MPI_Bcast
Broadcasts (sends) a message from the process with rank "root" to all other processes in the
group.
MPI_Bcast (&buffer,count,datatype,root,comm)
MPI_Scatter
Distributes distinct messages from a single source task to each task in the group.
255
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Scatter (&sendbuf,sendcnt,sendtype,&recvbuf,
...... recvcnt,recvtype,root,comm)
MPI_Gather
Gathers distinct messages from each task in the group to a single destination task. This routine
is the reverse operation of MPI_Scatter.
MPI_Gather (&sendbuf,sendcnt,sendtype,&recvbuf,
...... recvcount,recvtype,root,comm)
MPI_Allgather
Concatenation of data to all tasks in a group. Each task in the group, in effect, performs a oneto-all broadcasting operation within the group.
MPI_Allgather (&sendbuf,sendcount,sendtype,&recvbuf,
...... recvcount,recvtype,comm)
MPI_Reduce
Applies a reduction operation on all tasks in the group and places the result in one task.
MPI_Reduce (&sendbuf,&recvbuf,count,datatype,op,root,comm)
The predefined MPI reduction operations appear below. Users can also define their own
reduction functions by using the MPI_Op_create routine.
MPI Reduction Operation
C Data Types
MPI_MAX
maximum
integer, float
MPI_MIN
minimum
integer, float
MPI_SUM
su m
integer, float
MPI_PROD
product
integer, float
MPI_LAND
logical
AND
integer
MPI_BAND
bit-wise
AND
integer,
MPI_BYTE
MPI_LOR
logical
OR
integer
256
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_BOR
bit-wise
OR
integer,
MPI_BYTE
MPI_LXOR
logical
XOR
integer
MPI_BXOR
bit-wise
XOR
integer,
MPI_BYTE
MPI_MAXLOC
max
value and
location
float, double
and long
double
MPI_MINLOC
min value
and
location
float, double
and long
double
MPI_Allreduce
Applies a reduction operation and places the result in all tasks in the group. This is equivalent
to an MPI_Reduce followed by an MPI_Bcast.
MPI_Allreduce (&sendbuf,&recvbuf,count,datatype,op,comm)
MPI_Reduce_scatter
First does an element-wise reduction on a vector across all tasks in the group. Next, the result
vector is split into disjoint segments and distributed across the tasks. This is equivalent to an
MPI_Reduce followed by an MPI_Scatter operation.
MPI_Reduce_scatter (&sendbuf,&recvbuf,recvcount,datatype,
...... op,comm)
MPI_Alltoall
Each task in a group performs a scatter operation, sending a distinct message to all the tasks in
the group in order by index.
MPI_Alltoall (&sendbuf,sendcount,sendtype,&recvbuf,
...... recvcnt,recvtype,comm)
MPI_Scan
Performs a scan operation with respect to a reduction operation across a task group.
MPI_Scan (&sendbuf,&recvbuf,count,datatype,op,comm)
257
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Examples: Collective Communications
Perform a scatter operation on the rows of an array
C Language - Collective Communications Example
#include "mpi.h"
#include <stdio.h>
#define SIZE 4
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, sendcount, recvcount, source;
float sendbuf[SIZE][SIZE] = {
{1.0, 2.0, 3.0, 4.0},
{5.0, 6.0, 7.0, 8.0},
{9.0, 10.0, 11.0, 12.0},
{13.0, 14.0, 15.0, 16.0} };
float recvbuf[SIZE];
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
if (numtasks == SIZE) {
source = 1;
sendcount = SIZE;
recvcount = SIZE;
MPI_Scatter(sendbuf,sendcount,MPI_FLOAT,recvbuf,recvcount,
MPI_FLOAT,source,MPI_COMM_WORLD);
printf("rank= %d Results: %f %f %f %f\n",rank,recvbuf[0],
recvbuf[1],recvbuf[2],recvbuf[3]);
}
else
printf("Must specify %d processors. Terminating.\n",SIZE);
MPI_Finalize();
}
Sample program output:
rank= 0
rank= 1
Results: 1.000000 2.000000 3.000000 4.000000
Results: 5.000000 6.000000 7.000000 8.000000
258
∆.Σ. Βλάχος και Θ.Η. Σίµος
rank= 2
rank= 3
Results: 9.000000 10.000000 11.000000 12.000000
Results: 13.000000 14.000000 15.000000 16.000000
Derived Data Types
•
As previously mentioned, MPI predefines its primitive data types:
C Data Types
MPI_CHAR
MPI_SHORT
MPI_INT
MPI_LONG
MPI_UNSIGNED_CHAR
MPI_UNSIGNED_SHORT
MPI_UNSIGNED_LONG
MPI_UNSIGNED
MPI_FLOAT
MPI_DOUBLE
MPI_LONG_DOUBLE
MPI_BYTE
MPI_PACKED
•
•
•
MPI also provides facilities for you to define your own data structures based upon sequences
of the MPI primitive data types. Such user defined structures are called derived data types.
Primitive data types are contiguous. Derived data types allow you to specify non-contiguous
data in a convenient manner and to treat it as though it was contiguous.
MPI provides several methods for constructing derived data types:
o Contiguous
o Vector
o Indexed
o Struct
Derived Data Type Routines
259
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Type_contiguous
The simplest constructor. Produces a new data type by making count copies of an existing data
type.
MPI_Type_contiguous (count,oldtype,&newtype)
MPI_Type_vector
MPI_Type_hvector
Similar to contiguous, but allows for regular gaps (stride) in the displacements.
MPI_Type_hvector is identical to MPI_Type_vector except that stride is specified in bytes.
MPI_Type_vector (count,blocklength,stride,oldtype,&newtype)
MPI_Type_indexed
MPI_Type_hindexed
An array of displacements of the input data type is provided as the map for the new data type.
MPI_Type_hindexed is identical to MPI_Type_indexed except that offsets are specified in
bytes.
MPI_Type_indexed (count,blocklens[],offsets[],old_type,&newtype)
MPI_Type_struct
The new data type is formed according to completely defined map of the component data
types.
MPI_Type_struct (count,blocklens[],offsets[],old_types,&newtype)
MPI_Type_extent
Returns the size in bytes of the specified data type. Useful for the MPI subroutines that require
specification of offsets in bytes.
MPI_Type_extent (datatype,&extent)
MPI_Type_commit
Commits new datatype to the system. Required for all user constructed (derived) datatypes.
MPI_Type_commit (&datatype)
MPI_Type_free
Deallocates the specified datatype object. Use of this routine is especially important to prevent
memory exhaustion if many datatype objects are created, as in a loop.
MPI_Type_free (&datatype)
260
∆.Σ. Βλάχος και Θ.Η. Σίµος
Examples: Contiguous Derived Data Type
Create a data type representing a row of an array and distribute a different row to all
processes.
261
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Recv(b, SIZE, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &stat);
printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f\n",
rank,b[0],b[1],b[2],b[3]);
}
else
printf("Must specify %d processors. Terminating.\n",SIZE);
MPI_Type_free(&rowtype);
MPI_Finalize();
}
Sample program output:
rank=
rank=
rank=
rank=
0
1
2
3
b=
b=
b=
b=
1.0 2.0 3.0 4.0
5.0 6.0 7.0 8.0
9.0 10.0 11.0 12.0
13.0 14.0 15.0 16.0
Examples: Vector Derived Data Type
Create a data type representing a column of an array and distribute different columns to all
processes.
C Language - Vector Derived Data Type Example
#include "mpi.h"
#include <stdio.h>
#define SIZE 4
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, source=0, dest, tag=1, i;
float a[SIZE][SIZE] =
{1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0,
13.0, 14.0, 15.0, 16.0};
float b[SIZE];
MPI_Status stat;
MPI_Datatype columntype;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
262
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
MPI_Type_vector(SIZE, 1, SIZE, MPI_FLOAT, &columntype);
MPI_Type_commit(&columntype);
if (numtasks == SIZE) {
if (rank == 0) {
for (i=0; i<numtasks; i++)
MPI_Send(&a[0][i], 1, columntype, i, tag, MPI_COMM_WORLD);
}
MPI_Recv(b, SIZE, MPI_FLOAT, source, tag, MPI_COMM_WORLD, &stat);
printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f\n",
rank,b[0],b[1],b[2],b[3]);
}
else
printf("Must specify %d processors. Terminating.\n",SIZE);
MPI_Type_free(&columntype);
MPI_Finalize();
}
Sample program output:
rank=
rank=
rank=
rank=
0
1
2
3
b=
b=
b=
b=
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0 13.0
10.0 14.0
11.0 15.0
12.0 16.0
Examples: Indexed Derived Data Type
Create a datatype by extracting variable portions of an array and distribute to all tasks.
C Language - Indexed Derived Data Type Example
#include "mpi.h"
#include <stdio.h>
#define NELEMENTS 6
int main(argc,argv)
int argc;
char *argv[]; {
263
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
int numtasks, rank, source=0, dest, tag=1, i;
int blocklengths[2], displacements[2];
float a[16] =
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};
float b[NELEMENTS];
MPI_Status stat;
MPI_Datatype indextype;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
blocklengths[0] = 4;
blocklengths[1] = 2;
displacements[0] = 5;
displacements[1] = 12;
MPI_Type_indexed(2, blocklengths, displacements, MPI_FLOAT,
&indextype);
MPI_Type_commit(&indextype);
if (rank == 0) {
for (i=0; i<numtasks; i++)
MPI_Send(a, 1, indextype, i, tag, MPI_COMM_WORLD);
}
MPI_Recv(b, NELEMENTS, MPI_FLOAT, source, tag, MPI_COMM_WORLD,
&stat);
printf("rank= %d b= %3.1f %3.1f %3.1f %3.1f %3.1f %3.1f\n",
rank,b[0],b[1],b[2],b[3],b[4],b[5]);
MPI_Type_free(&indextype);
MPI_Finalize();
}
Sample program output:
rank=
rank=
rank=
rank=
0
1
2
3
b=
b=
b=
b=
6.0
6.0
6.0
6.0
7.0
7.0
7.0
7.0
8.0
8.0
8.0
8.0
9.0
9.0
9.0
9.0
13.0
13.0
13.0
13.0
14.0
14.0
14.0
14.0
Examples: Struct Derived Data Type
264
∆.Σ. Βλάχος και Θ.Η. Σίµος
Create a data type that represents a particle and distribute an array of such particles to all
processes.
C Language - Struct Derived Data Type Example
#include "mpi.h"
#include <stdio.h>
#define NELEM 25
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, source=0, dest, tag=1, i;
typedef struct {
float x, y, z;
float velocity;
int n, type;
}
Particle;
Particle
p[NELEM], particles[NELEM];
MPI_Datatype particletype, oldtypes[2];
int
blockcounts[2];
/* MPI_Aint type used to be consistent with syntax of */
/* MPI_Type_extent routine */
MPI_Aint
offsets[2], extent;
MPI_Status stat;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
/* Setup description of the 4 MPI_FLOAT fields x, y, z, velocity */
offsets[0] = 0;
oldtypes[0] = MPI_FLOAT;
blockcounts[0] = 4;
/* Setup description of the 2 MPI_INT fields n, type */
/* Need to first figure offset by getting size of MPI_FLOAT */
MPI_Type_extent(MPI_FLOAT, &extent);
offsets[1] = 4 * extent;
oldtypes[1] = MPI_INT;
blockcounts[1] = 2;
/* Now define structured type and commit it */
265
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Type_struct(2, blockcounts, offsets, oldtypes, &particletype);
MPI_Type_commit(&particletype);
/* Initialize the particle array and then send it to each task */
if (rank == 0) {
for (i=0; i<NELEM; i++) {
particles[i].x = i * 1.0;
particles[i].y = i * -1.0;
particles[i].z = i * 1.0;
particles[i].velocity = 0.25;
particles[i].n = i;
particles[i].type = i % 2;
}
for (i=0; i<numtasks; i++)
MPI_Send(particles, NELEM, particletype, i, tag,
MPI_COMM_WORLD);
}
MPI_Recv(p, NELEM, particletype, source, tag, MPI_COMM_WORLD,
&stat);
/* Print a sample of what was received */
printf("rank= %d
%3.2f %3.2f %3.2f %3.2f %d %d\n", rank,p[3].x,
p[3].y,p[3].z,p[3].velocity,p[3].n,p[3].type);
MPI_Type_free(&particletype);
MPI_Finalize();
}
Sample program output:
rank=
rank=
rank=
rank=
0
2
1
3
3.00
3.00
3.00
3.00
-3.00
-3.00
-3.00
-3.00
3.00
3.00
3.00
3.00
0.25
0.25
0.25
0.25
3
3
3
3
1
1
1
1
Group and Communicator Management
Routines
Groups vs. Communicators:
•
A group is an ordered set of processes. Each process in a group is associated with a unique
integer rank. Rank values start at zero and go to N-1, where N is the number of processes in
the group. In MPI, a group is represented within system memory as an object. It is accessible
266
∆.Σ. Βλάχος και Θ.Η. Σίµος
•
•
to the programmer only by a "handle". A group is always associated with a communicator
object.
A communicator encompasses a group of processes that may communicate with each other.
All MPI messages must specify a communicator. In the simplest sense, the communicator is
an extra "tag" that must be included with MPI calls. Like groups, communicators are
represented within system memory as objects and are accessible to the programmer only by
"handles". For example, the handle for the communicator that comprises all tasks is
MPI_COMM_WORLD.
From the programmer's perspective, a group and a communicator are one. The group routines
are primarily used to specify which processes should be used to construct a communicator.
Primary Purposes of Group and Communicator Objects:
1.
2.
3.
4.
Allow you to organize tasks, based upon function, into task groups.
Enable Collective Communications operations across a subset of related tasks.
Provide basis for implementing user defined virtual topologies
Provide for safe communications
Programming Considerations and Restrictions:
•
•
•
•
Groups/communicators are dynamic - they can be created and destroyed during program
execution.
Processes may be in more than one group/communicator. They will have a unique rank within
each group/communicator.
MPI provides over 40 routines related to groups, communicators, and virtual topologies.
Typical usage:
1. Extract handle of global group from MPI_COMM_WORLD using
MPI_Comm_group
2. Form new group as a subset of global group using MPI_Group_incl
3. Create new communicator for new group using MPI_Comm_create
4. Determine new rank in new communicator using MPI_Comm_rank
5. Conduct communications using any MPI message passing routine
6. When finished, free up new communicator and group (optional) using
MPI_Comm_free and MPI_Group_free
267
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
Group and Communicator Management
Routines
MPI_Comm_group
Determines the group associated with the given communicator.
MPI_Comm_group (comm,&group)
MPI_Group_rank
Returns the rank of this process in the given group or MPI_UNDEFINED if the process is not
a member.
MPI_Group_rank (group,&rank)
MPI_Group_size
Returns the size of a group - number of processes in the group.
MPI_Group_size (group,&size)
MPI_Group_excl
Produces a group by reordering an existing group and taking only unlisted members.
268
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Group_excl (group,n,&ranks,&newgroup)
MPI_Group_incl
Produces a group by reordering an existing group and taking only listed members.
MPI_Group_incl (group,n,&ranks,&newgroup)
MPI_Group_intersection
Produces a group as the intersection of two existing groups.
MPI_Group_intersection (group1,group2,&newgroup)
MPI_Group_union
Produces a group by combining two groups.
MPI_Group_union (group1,group2,&newgroup)
MPI_Group_difference
Creates a group from the difference of two groups.
MPI_Group_difference (group1,group2,&newgroup)
MPI_Group_compare
Compares two groups and returns an integer result that is MPI_IDENT if the order and
members of the two groups are the same, MPI_SIMILAR if only the members are the same,
and MPI_UNEQUAL otherwise.
MPI_Group_compare (group1,group2,&result)
MPI_Group_free
Frees a group
MPI_Group_free (group)
MPI_Comm_create
Creates a new communicator from the old communicator and the new group.
MPI_Comm_create (comm,group,&newcomm)
MPI_Comm_dup
Duplicates an existing communicator with all its associated information.
269
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Comm_dup (comm,&newcomm)
MPI_Comm_compare
Compares two communicators and returns integer result that is MPI_IDENT if the contexts
and groups are the same, MPI_CONGRUENT if different contexts but identical groups,
MPI_SIMILAR if different contexts but similar groups, and MPI_UNEQUAL otherwise.
MPI_Comm_compare (comm1,comm2,&result)
MPI_Comm_free
Marks the communicator object for deallocation.
MPI_Comm_free (&comm)
Examples: Group and Communicator Routines
Create two different process groups for separate collective communications exchange.
Requires creating new communicators also.
C Language - Group and Communicator Routines Example
#include "mpi.h"
#include <stdio.h>
#define NPROCS 8
int main(argc,argv)
int argc;
char *argv[]; {
int
rank, new_rank, sendbuf, recvbuf, numtasks,
ranks1[4]={0,1,2,3}, ranks2[4]={4,5,6,7};
MPI_Group orig_group, new_group;
MPI_Comm
new_comm;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
if (numtasks != NPROCS) {
printf("Must specify MP_PROCS= %d. Terminating.\n",NPROCS);
MPI_Finalize();
exit(0);
}
270
∆.Σ. Βλάχος και Θ.Η. Σίµος
sendbuf = rank;
/* Extract the original group handle */
MPI_Comm_group(MPI_COMM_WORLD, &orig_group);
/* Divide tasks into two distinct groups based upon rank */
if (rank < NPROCS/2) {
MPI_Group_incl(orig_group, NPROCS/2, ranks1, &new_group);
}
else {
MPI_Group_incl(orig_group, NPROCS/2, ranks2, &new_group);
}
/* Create new new communicator and then perform collective
communications */
MPI_Comm_create(MPI_COMM_WORLD, new_group, &new_comm);
MPI_Allreduce(&sendbuf, &recvbuf, 1, MPI_INT, MPI_SUM, new_comm);
MPI_Group_rank (new_group, &new_rank);
printf("rank= %d newrank= %d recvbuf= %d\n",rank,new_rank,recvbuf);
MPI_Finalize();
}
Sample program output:
rank=
rank=
rank=
rank=
rank=
rank=
rank=
rank=
7
0
1
2
6
3
4
5
newrank=
newrank=
newrank=
newrank=
newrank=
newrank=
newrank=
newrank=
3
0
1
2
2
3
0
1
recvbuf=
recvbuf=
recvbuf=
recvbuf=
recvbuf=
recvbuf=
recvbuf=
recvbuf=
22
6
6
6
22
6
22
22
Virtual Topologies
What Are They?
•
•
In terms of MPI, a virtual topology describes a mapping/ordering of MPI processes into a
geometric "shape".
The two main types of topologies supported by MPI are Cartesian (grid) and Graph.
271
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
•
•
•
MPI topologies are virtual - there may be no relation between the physical structure of the
parallel machine and the process topology.
Virtual topologies are built upon MPI communicators and groups.
Must be "programmed" by the application developer.
Why Use Them?
•
•
Convenience
o Virtual topologies may be useful for applications with specific communication
patterns - patterns that match an MPI topology structure.
o For example, a Cartesian topology might prove convenient for an
application that requires 4-way nearest neighbor communications for
grid based data.
Communication Efficiency
o Some hardware architectures may impose penalties for communications between
successively distant "nodes".
o A particular implementation may optimize process mapping based upon the physical
characteristics of a given parallel machine.
o The mapping of processes into an MPI virtual topology is dependent upon the MPI
implementation, and may be totally ignored.
Example:
A simplified mapping of processes into a Cartesian virtual topology appears below:
Virtual Topology Routines
MPI_Cart_coords
Determines process coordinates in Cartesian topology given rank in group.
272
∆.Σ. Βλάχος και Θ.Η. Σίµος
MPI_Cart_coords (comm,rank,maxdims,&coords[])
MPI_Cart_create
Creates a new communicator to which Cartesian topology information has been attached.
MPI_Cart_create (comm_old,ndims,&dims[],&periods,
...... reorder,&comm_cart)
MPI_Cart_get
Retrieves the number of dimensions, coordinates and periodicity setting for the calling process
in a Cartesian topology.
MPI_Cart_get (comm,maxdims,&dims,&periods,&coords[])
MPI_Cart_map
Maps process to Cartesian topology information
MPI_Cart_map (comm_old,ndims,&dims[],&periods[],&newrank)
MPI_Cart_rank
Determines process rank in communicator given the Cartesian coordinate location.
MPI_Cart_rank (comm,&coords[],&rank)
MPI_Cart_shift
Returns the shifted source and destination ranks for the calling process in a Cartesian
topology. Calling process specifies the shift direction and amount.
MPI_Cart_shift (comm,direction,displ,&source,&dest)
MPI_Cart_sub
Partitions a communicator into subgroups that form lower-dimensional Cartesian subgrids
MPI_Cart_sub (comm,&remain_dims[],&comm_new)
MPI_Cartdim_get
Retrieves the number of dimensions associated with a Cartesian topology communicator.
MPI_Cartdim_get (comm,&ndims)
273
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
MPI_Dims_create
Creates a division of processors in a Cartesian grid.
MPI_Dims_create (nnodes,ndims,&dims[])
MPI_Graph_create
Makes a new communicator to which topology information has been attached.
MPI_Graph_create (comm_old,nnodes,&index[],&edges[],
...... reorder,&comm_graph)
MPI_Graph_get
Retrieves graph topology information associated with a communicator.
MPI_Graph_get (comm,maxindex,maxedges,&index[],&edges[])
MPI_Graph_map
Maps process to graph topology information.
MPI_Graph_map (comm_old,nnodes,&index[],&edges[],&newrank)
MPI_Graph_neighbors
Returns the neighbors of a node associated with a graph topology.
MPI_Graph_neighbors (comm,rank,maxneighbors,&neighbors[])
MPI_Graphdims_get
Retrieves graph topology information (number of nodes and number of edges) associated with
a communicator
MPI_Graphdims_get (comm,&nnodes,&nedges)
MPI_Topo_test
Determines the type of topology (if any) associated with a communicator.
MPI_Topo_test (comm,&top_type)
Examples: Cartesian Virtual Topology
274
∆.Σ. Βλάχος και Θ.Η. Σίµος
Create a 4 x 4 Cartesian topology from 16 processors and have each process exchange its rank
with four neighbors.
C Language - Cartesian Virtual Topology Example
#include "mpi.h"
#include <stdio.h>
#define SIZE 16
#define UP
0
#define DOWN 1
#define LEFT 2
#define RIGHT 3
int main(argc,argv)
int argc;
char *argv[]; {
int numtasks, rank, source, dest, outbuf, i, tag=1,
inbuf[4]={MPI_PROC_NULL,MPI_PROC_NULL,MPI_PROC_NULL,MPI_PROC_NULL,},
nbrs[4], dims[2]={4,4},
periods[2]={0,0}, reorder=0, coords[2];
MPI_Request reqs[8];
MPI_Status stats[8];
MPI_Comm cartcomm;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
if (numtasks == SIZE) {
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reorder,
&cartcomm);
MPI_Comm_rank(cartcomm, &rank);
MPI_Cart_coords(cartcomm, rank, 2, coords);
MPI_Cart_shift(cartcomm, 0, 1, &nbrs[UP], &nbrs[DOWN]);
MPI_Cart_shift(cartcomm, 1, 1, &nbrs[LEFT], &nbrs[RIGHT]);
outbuf = rank;
for (i=0; i<4; i++) {
dest = nbrs[i];
source = nbrs[i];
MPI_Isend(&outbuf, 1, MPI_INT, dest, tag,
MPI_COMM_WORLD, &reqs[i]);
MPI_Irecv(&inbuf[i], 1, MPI_INT, source, tag,
MPI_COMM_WORLD, &reqs[i+4]);
275
..
..
..
..
..
Παράλληλος προγραµµατισµός σε C µε χρήση MPI
}
MPI_Waitall(8, reqs, stats);
printf("rank= %d coords= %d %d neighbors(u,d,l,r)= %d %d %d
%d\n",
rank,coords[0],coords[1],nbrs[UP],nbrs[DOWN],nbrs[LEFT],
nbrs[RIGHT]);
printf("rank= %d
inbuf(u,d,l,r)= %d %d %d %d\n",
rank,inbuf[UP],inbuf[DOWN],inbuf[LEFT],inbuf[RIGHT]);
}
else
printf("Must specify %d processors. Terminating.\n",SIZE);
MPI_Finalize();
}
Sample program output: (partial)
rank=
rank=
rank=
rank=
rank=
rank=
0 coords= 0 0
0
1 coords= 0 1
1
2 coords= 0 2
2
. . . . .
rank=
rank=
rank=
rank=
14 coords= 3 2
14
15 coords= 3 3
15
neighbors(u,d,l,r)= -3
inbuf(u,d,l,r)= -3 4
neighbors(u,d,l,r)= -3
inbuf(u,d,l,r)= -3 5
neighbors(u,d,l,r)= -3
inbuf(u,d,l,r)= -3 6
4 -3 1
-3 1
5 0 2
0 2
6 1 3
1 3
neighbors(u,d,l,r)= 10 -3 13 15
inbuf(u,d,l,r)= 10 -3 13 15
neighbors(u,d,l,r)= 11 -3 14 -3
inbuf(u,d,l,r)= 11 -3 14 -3
276