.. .. .. .. .. Πανεπιστήµιο Πελοποννήσου Παράλληλος Προγραµµατισµός . . . . . . σε 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; Ανάλυση χρόνου Ας δούµε τώρα µια εκτίµηση για το χρόνο που κάνει να τρέξει ο παράλληλος αλγόριθµος. Αν χ είναι ο χρόνος για τον υπολογισµό ενός γινοµένου, τότε ο χρόνος που διαρκεί το υπολογιστικό µέρος του αλγόριθµου είναι χnn/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. Ο συνολικός αριθµός των στοιχείων είναι nlogp. Έτσι, η συνολική πολυπλοκότητα του αλγόριθµου είναι Θ(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/√pxn/√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
© Copyright 2024 Paperzz