Lab: MPI

Table of Contents

SSH keys

The idea

One of the key differences between parallel computing and distributed computing is that you're introducing more than one computer into the mix. This means that you need to log into other computers, automatically, yet securely. Passwords are terrible for this: do you put your password into code, where anyone can read them? Instead, we use an approach called Secure Shell (abbreviated as SSH) as a way of securely logging into another computer without the need for a password.

Here's the idea, for those of you that haven't come across this before: you're going to use a program called ssh-keygen that will generate two files of random-seeming characters. One of those files is called a "private key," and you keep it hidden from everyone else. The other file is called a "public key," and you can share its contents with the world. By using some really clever encryption algorithms, you can demonstrate to anyone holding the public key that you've got the matching private key, without actually having to divulge the contents of the private key file. So that's how you can securely connect to other computers. First, you share your public key with them. After you've done so, you can prove that you're the holder of the private key, and hence it's ok for you to log in.

Version 1: Setting things up on a lab machine

Step 1: generate the keys

You will now generate a private and public key pair so that you can securely connect from one department computer to another, without having to type in a password.

The instructions that follow describe how to create a single key pair with essentially default settings. If you have already done this for some other reason, you might need to use keys you already have, or you might wish to create a separate pair of keys with different names. If so, skip the below steps and instead proceed as appropriate. If you don't already know how to figure out how to proceed, ask for help.

Log into a lab computer. At the terminal window, issue the following command:

$ ssh-keygen -t rsa

You'll be prompted a few times for a few different things, but just keep hitting the Enter key and going with the defaults. In particular, you'll be prompted twice for a passphrase. Leave the passphrase blank. This has nothing to do with your Carleton password; it's giving you the ability to put an extra level of encryption on top of your key pair. Don't do it, as it will make automating connecting to other machines much harder. You are still plenty secure without it.

Look in your .ssh directory by issuing a cd .ssh command. You should find files id_rsa (that's your private key) and id_rsa.pub (that's your public key). Take a look at them if you like by issuing commands cat id_rsa and cat id_rsa.pub. Enjoy the gobbledy-gook.

This lab is for you to learn a little about MPI, the Message Passing Interface. We'll be using Open MPI, an implementation of MPI that has been made available for a number of languages, including Java. The documentation is a little challenging to read. The best and most detailed source is the Open MPI API, but the catch is that it is all written for the C++ version of the library. The concepts are correct, but the syntax is for C++. For the actual Java API that we'll be using, the documentation is strangely not anywhere centralized online, but I've generated a version here. You'll find that the Java API documentation is Java friendly, but is missing the longer details on how things work that the C++ API contains.

Finally, this MPI tutorial is very helpful. Again, it's in C++, but it does a good job explaining the ideas.

Step 2: Copy the public key

In order to gain access to a remote computer by using the pairs, we need to add the public key to a file called ~/.ssh/authorized_keys on the remote computer. Remember that the ~ is an abbreviation for our home directory, so the authorized_keys file is in a subdirectory of your home directory. So in principle we need to append your public key to the ~/.ssh/authorized_keys file on every computer you want to connect to. But here's a neat trick. As long as we're connecting to lab computers in the department, they all have the same authorized_keys file, because it's in a subdirectory of your home directory, and your home directory is exactly the same files on all department machines. So for this particular scenario, we can add your public key to the ~/.ssh/authorized_keys file for the same computer that you're on. To do that, issue the following commands:

cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

The cat command just echoes the contents of a file. The >> means to take that output and append it to the file that follows.

Step 3: Connect to another computer in the department

Assuming that the above is all done correctly, you should now be able to connect to another computer in the department remotely. Let's try it!

At the command prompt, issue the command ssh cmc000-00, but instead of cmc000-00, put in the name of one of the computers next to you. For example, if the computer next to you was cmc304-32, you would type ssh cmc304-02.

You should receive a warning that looks something like this:

The authenticity of host 'cmc000-00 (137.22.1.1)' can't be established.
ECDSA key fingerprint is SHA256:dsjflkjdlkgjfdsAKJHKJhkjhkJHKJhklj.
Are you sure you want to continue connecting (yes/no)?

Type yes. This only happens the first time you connect to a new computer, and it is an extra security step to confirm that you haven't done this before.

If all works, you should then see a command prompt indicating that you are logged into a different computer. Logout (by typing exit) at the prompt, then try to connect again. The second time, you should get no prompt, and you should just automatically connect. Try connecting to a few other computers and make sure you can do it.

Ask for help if you're having trouble!

Step 4: Do it all again for your partner

If you are working with a partner, repeat the whole process for your partner. This way, either of you can do this if you are playing around on your own. Here's a hint: you don't actually have to log out of the whole desktop. At a terminal window, type the command

$ whoami

which should show you the name of the user is logged in. But you can use ssh to log in another user, with a password if you haven't set up ssh keys for that person yet. For example, if I wanted to log into the same computer from the command prompt, I would type:

$ ssh dmusicant@cmc000-00

where again, cmc000-00 is the name of the computer that you're actually on.

Hello World

Finally, let's move on and start working with some actual code.

Here's the code for a simple Hello World program:

import mpi.*;
import java.net.*;

public class HelloWorld {

    public static void main(String[] args) throws MPIException, UnknownHostException {

        MPI.Init(args);

        int size = MPI.COMM_WORLD.getSize();
        int rank = MPI.COMM_WORLD.getRank();
        String hostname = InetAddress.getLocalHost().getHostName();

        System.out.println("Hello world from host " + hostname + ", rank " + rank + " out of " + size);

        MPI.Finalize();
    }
}

Copy this code into a file called HelloWorld.java, and save it somewhere appropriate. You might find it easier to just use your favorite standalone editor rather than IntelliJ, since we'll be running this code from the command-line. (I could set up a whole Maven configuration to make all of this happen, but you'll learn more by seeing how it actually works.)

Compile and run it with the following commands:

javac -cp .:/usr/local/openmpi-3.1.1/lib/mpi.jar HelloWorld.java
mpirun-cmc java HelloWorld

This should run two processes (the default number), on your computer, displaying output for each.

Since your computers have four cores, you'll want to configure MPI to allow you to run 4 cores on your machine. To do that, you'll need to create a hostfile. Create a file in the same directory you're working in called hostfile, and put in this content:

localhost slots=4

Then run your program again, but this time directing MPI to use your hostfile:

mpirun-cmc --hostfile hostfile java HelloWorld

You should get hellos from four distinctly numbered processes.

One of the fabulous capabilities of MPI is that in can run parallel jobs on multiple computers easily. Split up HelloWorld so that it runs on your computer as well as the computers next to you. To do so, modify your hostfile so it now looks like (but modify the computer names so it is the computers next to you):

localhost slots=1
cmc304-04 slots=1
cmc304-05 slots=1
cmc304-06 slots=1

Before you actually run your program with the above hostfile, first make sure that you manually ssh into each of the computers at the command line. The first time you ssh into another computer, you have to answer "yes" to the "The authenticity of host…" prompt. MPI won't successfully launch jobs on another computer until you've cleared the above hurdle.

Once you've successfully manually ssh'ed into each computer in your hostfile once, then run your program using mpirun-cmc. You've just run a program on multiple machines at the same time!

Sending and Receiving Messages

You can send and receive messages using the the send and recv methods of MPI.COMM_WORLD. These methods both take Buffer (of which IntBuffer is a subclass), which is much like an array but is optimized for transfering data. For example, here is a program in which the root process (process 0) sends a message to process 1. Try it out.

import mpi.*;
import java.nio.*;

public class SendRecv {

    public static void main(String[] args) throws MPIException{

        MPI.Init(args);

        int size = MPI.COMM_WORLD.getSize();
        int rank = MPI.COMM_WORLD.getRank();

        if (size < 2) {
            System.out.println("Too few processes!");
            System.exit(1);
        }

        IntBuffer buffer = MPI.newIntBuffer(1);

        int numItemsToTransfer = 1;
        int sourceRank = 0;
        int destinationRank = 1;
        int messageTag = 0;  // supplemental tag, often just not used
        int bufferPosition = 0;

        if (rank == 0) {
            int valueToTransmit = 5;
            buffer.put(bufferPosition, valueToTransmit);
            MPI.COMM_WORLD.send(buffer, numItemsToTransfer, MPI.INT,
                                destinationRank, messageTag);
        } else if (rank == 1) {
            Status status = MPI.COMM_WORLD.recv(buffer, numItemsToTransfer,
                                                MPI.INT, sourceRank, messageTag);
            System.out.println("Process 1 received value " +
                               buffer.get(bufferPosition) + " from process 0");
        }

        MPI.Finalize();
    }
}

Exercise 1

Write a program that sends a value (perhaps a -1) in a circle, starting with process zero, and going through all the processes until it ends up back at 0 again. Here is what sample output should look like:

Process 1 received token -1 from process 0
Process 2 received token -1 from process 1
Process 3 received token -1 from process 2
Process 4 received token -1 from process 3
Process 0 received token -1 from process 4

Make sure to avoid deadlock. Whenever each process receives a value, it should print a line of text indicating which process it is, and what the value was.

One challenge you may or may not run into in the above exercise is that the results of the output may be interleaved, sometimes out of order. System.out.println is synchronized and thread-safe, but here we have multiple single-threaded processes all writing into the same output buffer, and the timing on how the results get flushed may not happen in the order that you expect. If you think that the terminal window is showing you the output in an incorrect order, add a short Thread.sleep call somewhere in your code so that each process takes a short break before sending the value off to the next process.

Broadcast communication

MPI's collective functions allow communications among all processes in a group. Broadcast sends a message to every process. Reduce collects values from all processes and aggregates them.

Here is some sample code that uses broadcast and reduce functionality. Copy the code into a file, and run it. See if you can figure out line-by-line what it's doing. Ask questions if you have any.

import mpi.*;
import java.nio.*;

public class BcastReduce {

    public static void main(String[] args) throws MPIException {

        MPI.Init(args);

        int size = MPI.COMM_WORLD.getSize();
        int rank = MPI.COMM_WORLD.getRank();
        String name = MPI.COMM_WORLD.getName();

        IntBuffer buffer = MPI.newIntBuffer(1);

        int numItemsToTransfer = 1;
        int sourceRank = 0;
        int bufferPosition = 0;
        int numberToPlayWith = 5;

        if (rank == sourceRank) {
            buffer.put(bufferPosition, numberToPlayWith * 3);
        }

        MPI.COMM_WORLD.bcast(buffer, numItemsToTransfer, MPI.INT, sourceRank);

        int valueReceived = buffer.get(0);

        int valueModified = valueReceived + 4;

        buffer.put(bufferPosition, valueModified);

        IntBuffer finalAnswer = MPI.newIntBuffer(1);
        int destProcess = 0;
        MPI.COMM_WORLD.reduce(buffer, finalAnswer, numItemsToTransfer,
                              MPI.INT, MPI.SUM, destProcess);
        if (rank == 0) {
            System.out.println("Final answer = " + finalAnswer.get(0));
        }

        MPI.Finalize();

    }
}

Exercise 2

A classic example is computing the value of pi using numerical integration. Basically, we trying to calculate the area under the curve \(f(x) = 4 / (1 + x^2)\) between \(x = 0\) and \(1\), which happens to be pi. The more intervals we divide it into, the more accurate the answer. The root process broadcasts the number of intervals (n) to all processes, which compute part of the sum, then the reduce function does the summation.

Here is pseudocode for the algorithm to compute pi; see if you can translate it to MPI! In case you are wondering if your solution is right, the answer I get for \(n = 1000\) is \(3.141593\).

root process only:
    ask the user for a value n
broadcast the value n to all processes
width <- 1 / n
sum = 0
forall 1 <= i <= n (in parallel - divide the i values between processes somehow):
    sum <- sum + 4 / (1 + (width * (i - 0.5)) ** 2)
mypi <- width * sum
sum all processes' mypi using reduce
print sum

k-means clustering

K-means clustering is an algorithm used to find a small number of examples that work well to summarize a larger dataset. Here is one explanation of k-means. For this task, start with this k-means Java program that I have written that performs k-means clustering on an input dataset. Your job is to use MPI to run k-means in a distributed fashion across multiple computers. You should first look through my code, and think about what aspects of the code can be parallelized. You should then modify the code accordingly to use MPI to do so.

There are lots of examples of using MPI to do k-means online. I would encourage you not to look at them, but I won't expressly forbid it. One critical point, however, is that it is my expectation for this assignment that you will minimally modify my program to introduce the MPI aspects needed to get it to run in a distributed fashion. We won't grant credit for a submission that does k-means clustering via MPI, but is clearly a different program. In particular, the output that your MPI version generates should be identical to mine, apart from possible rounding error.

I have very intentionally left the details of this vague, as to give you some space to stretch out and think about how you want to do this. That said, ask for help if you need it: the Moodle Q&A is a great place to post questions.

You'll need some data to work on. This short Python3 program (requires the sklearn module) will generate data for you. It starts with a randomly generated set of cluster centers, and then generates a lot of data spread around those centers randomly.

Danger, danger, beware, warnings ahead

One important thing to note is that the above data generator dumps the output to the /tmp directory. It is critical that you leave your randomly generated data there and don't move it to your department home directories. /tmp is a local directory on every computer, whereas your home directory exists on a single server and is mounted by every computer you log into. If you copy your generated data to your home directory or any subdirectory, three very bad things happen:

  1. You will clog up our limited server space with gigabytes of random data. You collectively run the risk of filling up the server, and preventing CS students in all classes from being able to save their work.
  2. You will clog up our network connections as you read gigabytes of data from these files. You collectively run the risk of overloading our network, preventing CS students in all classes from being able to access their work.
  3. This is the worst of all: Mike Tie will come after you.

If you are using multiple computers, since /tmp is not shared between them, you'll need to copy the file to /tmp on all computers that you're using. Don't copy it to your home directory as a temporary spot. (If you goof and do so, don't panic. Just delete it right away.) To get the data on multiple computers, there are a variety of options. Here are two: you can use the scp program from the command line, which lets you use ssh to copy files from one computer to another. Online tutorials are plentiful; here is one scp tutorial. Another approach is just to rerun the data generator program on each computer. I'm using a seed in there for the random number generator, so it will create the same file each time.

More danger and beware warnings

While you're working on your assignment, you may be using multiple computers. Make sure that you aren't slowing down other people from working. You can completely test your code by running MPI only on your local computer, i.e. by setting up your hostfile so that it only runs locally. You'll undoubtedly want to try it eventually on multiple computers to see it fly, but only do that at the end, and make sure that others aren't using those computers at the time. One approach (besides just looking in the labs) is to ssh into a machine and type just a single w at the command prompt. It will show you the logged in users. You should see yourself; if you see others, the machine is being used.

Enjoy, and have fun with it!