Inhaltsverzeichnis

Apfelmännchen in Open MPI

Vorbereitungen

Quellcode

Das OpenMP-Programm wurde für MPI nach einem Client/Server-Ansatz umgeschrieben. Vom Master-Prozess werden einzelne Zeilen(nummern) an Slave-Prozesse übergeben, die berechnete Zeilen als Felder zurückliefern.

//: mpi_mandelbrot.cpp : Demo Open MP / Open MPI - R.Richter 2011-01-20
///////////////////////////////////////////////////////////////////////
// g++ -fopenmp ... für Open MP
// mpic++ ...       für Open MPI
 
#define USE_MPI
 
#include <iostream>
#include <complex>
#include "image.h"
 
#ifdef USE_MPI
  #include <mpi.h>
  char processorName[MPI_MAX_PROCESSOR_NAME] = "myHostName";
#endif
 
#define MASTER   0
#define GOTOWORK 1
#define GOTODIE  2
 
typedef std::complex<double> complex;
 
int compute(complex c, int maxIterations)
{
  int count = 0;
  complex z; 
 
  while (abs(z) <= 2.0 && count < maxIterations) 
  {
    z = z * z + c;
    ++ count;
  }
  return count;
}
 
inline 
Color color(int height, int max)
{
  // color scheme from: 
  // http://shreyassiravara.wordpress.com/2010/08/14/the-mandelbrot-set/
  if (height >= max) return Color::BLACK;
  double h = 255 * log(double(height)) / log(double(max)); 
  return Color(0.9 * h, 0.8 * h, 0.6 * h);
}
 
inline
double scale(int pos, int length, double low, double high)
{
  return low + pos * (high-low) / (length-1);
}
 
#ifdef USE_MPI
 
void send(int line, int size, int task, int tag)
{
  MPI_Send(&line, size, MPI_INT, task, tag, MPI_COMM_WORLD);
  if (tag == GOTOWORK) 
  {
    std::cout << "Line " << line 
              << " sent to process " << task << std::endl;
  }
}
 
int incoming(std::vector<int>& results)
{
  MPI_Status status;
  MPI_Recv(&results[0], results.size(), MPI_INT, 
           MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
 
  int task = status.MPI_SOURCE;
  std::cout << "Line " << results[results.size() - 1] 
            << " received from process " << task 
            << " (" << processorName << ")" << std::endl;
  return task;  
}
 
void process(Image& image, std::vector<int> const& iterations, int max)
{
  int const width = iterations.size() - 1;
  int const y = iterations[width];
  for (int x = 0; x < width; ++x)
  {
    image.pixel(x, y) = color(iterations[x], max);   
  }
}
 
#endif // USE_MPI
 
void master(int nProcesses)
{
  std::cout << "starting master of " << nProcesses << " tasks" << std::endl;
 
  int width  = 500;
  int height = 500;
  Image image(width, height);
  int maxIterations = 10000;
  complex left_bottom(-2.0, -2.0);
  complex right_top  ( 2.0,  2.0);
 
#ifndef USE_MPI
 
  // serial program or shared-memory parallelism with Open MP:
  #pragma omp parallel for
  for (int y = 0; y < height; ++y)
  {
    for (int x = 0; x < width; ++x)
    {
      complex c(scale(x, width,  real(left_bottom), real(right_top)),
                scale(y, height, imag(left_bottom), imag(right_top)));
 
      int iterations = compute(c, maxIterations);
      image.pixel(x, y) = color(iterations, maxIterations);   
    }
  }
 
#else // USE_MPI
 
  // broadcast global data to all other processes
  int    buffer1[3] = { width, height, maxIterations };
  double buffer2[4] = { real(left_bottom), imag(left_bottom),
                        real(right_top), imag(right_top) };
 
  MPI_Bcast(buffer1, 3, MPI_INT,    MASTER, MPI_COMM_WORLD);
  MPI_Bcast(buffer2, 4, MPI_DOUBLE, MASTER, MPI_COMM_WORLD);
 
  // distribute first lines to all other processes
  int line = 0;
  for (int task = 1; task < nProcesses; ++task, ++line)
  {
    send(line, 1, task, GOTOWORK);
  }
 
  std::vector<int> results(width + 1); // iterations + lineNr
 
  for ( ; line < height; ++line)  // until all lines processed 
  {
    int task = incoming(results);
    send(line, 1, task, GOTOWORK);
    process(image, results, maxIterations);
  }
 
  for (int task = 1; task < nProcesses; ++task)  // all outstanding tasks
  {
    incoming(results);
    send(0, 0, task, GOTODIE);
    process(image, results, maxIterations);
  }
 
#endif // USE_MPI
 
  saveBMP("mandel.bmp", image);
}
 
void slave(int myId)
{
  std::cout << "starting slave " << myId << std::endl;
#ifdef USE_MPI
 
  // get global data broadcasted from master
  int    buffer1[3];
  double buffer2[4];
 
  MPI_Bcast(buffer1, 3, MPI_INT,    MASTER, MPI_COMM_WORLD);
  MPI_Bcast(buffer2, 4, MPI_DOUBLE, MASTER, MPI_COMM_WORLD);
 
  int width           = buffer1[0];
  int height          = buffer1[1];
  int maxIterations   = buffer1[2];
  complex left_bottom ( buffer2[0], buffer2[1]);
  complex right_top   ( buffer2[2], buffer2[3]);
 
  while (true)
  {
    // get work : line number
    MPI_Status status;
    int y;
    MPI_Recv(&y, 1, MPI_INT, 
             MASTER, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
 
    if (status.MPI_TAG == GOTODIE) break;   
 
    // do work : compute a vector of data
    std::vector<int> iterations(width + 1);
    iterations[width] = y; // store line number for return here  
 
    for (int x = 0; x < width; ++x)
    {
      complex c(scale(x, width,  real(left_bottom), real(right_top)),
                scale(y, height, imag(left_bottom), imag(right_top)));
 
      iterations[x] = compute(c, maxIterations);
    }
    // send results
    MPI_Send(&iterations[0], width+1, MPI_INT, MASTER, 0, MPI_COMM_WORLD);
  }
 
#endif // USE_MPI
}
 
int main(int argc, char* argv[])
{
  int nProcesses = 1;
  int myId = MASTER;
 
#ifdef USE_MPI  
  MPI_Init(&argc, &argv);
  MPI_Comm_size(MPI_COMM_WORLD, &nProcesses);
  MPI_Comm_rank(MPI_COMM_WORLD, &myId);  
  int nameLength = MPI_MAX_PROCESSOR_NAME;
  MPI_Get_processor_name(processorName, &nameLength);
#endif
 
  std::cout << "Hello from process " << myId 
            << " of " << nProcesses << std::endl;
 
  if (myId == MASTER) master(nProcesses);
  else                slave(myId);
 
#ifdef USE_MPI
  MPI_Finalize();
#endif
  return 0;
}

Übersetzung, Konfiguration, Ausführung

Übersetzen

mpic++ hello.cpp -o hello

Ausführen

Das Programm kann lokal unter Angabe der gewünschten Prozessanzahl (beim Master/slave-Ansatz mindestens 2) gestartet werden.

mpirun -np 8 mpi_mandelbrot 

Zum verteilten Ausführen muss das Programm auf allen beteiligten Maschinen vorhanden sein und passwortloser SSH-Zugang (siehe unten) auf die Maschinen möglich sein. Das Machinefile listet die beteiligten Maschinen mit Hostname oder IP-Addresse auf:

# my_hostfile 
192.168.0.205
192.168.0.208 slots=4 max_slots=8

Die Angabe der (maximal) zu startenden Prozesse ist optional. Der Programmstart erfolgt mit

mpirun --hostfile my_hostfile mpi_mandelbrot

oder

mpirun --machinefile my_hostfile mpi_mandelbrot

Die Ausgaben aller Programme erscheinen auf der Konsole des MPI-Hauptprozesses.

passwortloser SSH-Zugang

1. auf dem Master-System ein SSH-Schlüsselpaar erzeugen (ablegen unter ~/.ssh)

ssh-keygen -t rsa

2. mit Editor Inhalt von ~/.ssh/rsa.pub auf Zielsystem in ~/.ssh/authorized_keys2 einfügen

Zeitmessung

Die Ausführung erfolgte in Ubuntu innerhalb eines VirtualBox-Systems.

Die MPI-Version benötigte auf der Einzelmaschine mit 8 Prozessen 100 Sekunden. Zum Vergleich dazu die OpenMP-Version 88 Sekunden. Da VirtualBox nur einen CPU-Kern des Hostsystems (AMD Turion 64, 2.1 GHz unter Windows 7 64bit) auslastet, ist dieser Zeitbedarf mit einem Einzelprozess auf dem Hostsystem vergleichbar.

Auf mehrere Maschinen (virtuelle und native Linux-Systeme) verteilt, verkürzt sich die Gesamtrechenzeit. Vor der ersten Rückmeldung der Client-Prozesse vergehen allerdings einige Sekunden für den Aufbau der SSH-Verbindung.

Weiterführende Quellen