Setting up a GROMACS cluster

computing
molecular dynamics
skills
Author

Philip Fowler

Published

April 28, 2016

Recently I've moved to the John Radcliffe hospital and my old lab kindly let me have some old servers that were switched off. This pushed me to learn how to setup them up as a compute cluster with a scheduler for running GROMACS jobs. I've wanted to learn this for years, having used many clusters myself, but haven't plucked up the courage until now. This post is a detailed walk-through on how I chose to do this. During the process I did a lot of Googling and have written the post I would have liked to have found; long, comprehensive and a bit verbose. Since it is long so let's break it down into four tasks. [caption id="attachment_3942" align="alignright" width="261"]Fingerless gloves and a woolly hat can be useful in a cold, noisy machine room Fingerless gloves and a woolly hat can be useful in a cold, noisy machine room[/caption] 1. Install Ubuntu on each machine 2. Setup networking, including sharing directories on the headnode via NFS 3. Using environment modules, compile GROMACS into one of the shared directories so all the machines in the cluster can run lmx 4. Install SLURM

1. Install Ubuntu

[caption id="attachment_3931" align="alignright" width="373"] Untangling ethernet cables can be surprisingly tricky[/caption] On the Apple Xserves, the only way I have found of doing this is via CD or USB stick. Because they are EFI-boot machines I cannot get PXE booting to work and therefore I've never been able to get ROCKS or OSCAR working. Since I am sitting in a cold and noisy machine room, I had hoped to install say four nodes at once using four CDs, however, I've found that if you swap the monitor from one machine to another (by unplugging the mini DisplayPort-to-VGA adaptor from the back of the machine), it won't work when you reconnect later to see how the install is going. Since I only have two monitors, that limits me to two installations at a time. Installation time from CD was 18 minutes, from USB stick around 14 minutes. I used Ubuntu 14.04.4 Server. Since I am setting up the cluster just for me to begin with, I setup the same user with the same password on each machine - this user is given a UID and GID of 1000 by default. During the process you have to make various decisions. For the record I chose a guided partition of the whole disc using LVM, to automatically install security updates and to install the open-ssl and mail servers. The latter to be configured as a local mail server using the hostname. As we are talking about hostnames, this is what I chose for the private network.

hostname Private IP
headnode 192.168.0.100
node01 192.168.0.1
node02 192.168.0.2
... ...
node15 192.168.0.15

Update the packages on each machine,

$ sudo apt-get update -y && sudo apt-get upgrade -y

There are also some common packages each machine will need

$ sudo apt-get install environment-modules htop slurm-llnl

Finally, each machine will need a mount point for the NFS software share

$ sudo mkdir /opt/software

2. Setup networking

We need to setup the cluster so each compute node gets assigned a static private IP by the headnode using DHCP and then each compute node can see the outside network by communicating via the headnode using NAT. Most of the work here is on the headnode.

Headnode

At present the public IP of the headnode is assigned by DHCP. Edit /etc/network/interfaces.

$ sudo vim /etc/network/interfaces

The file looks like

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

auto eth1
iface eth1 inet static
address 192.168.0.100
network 192.168.0.0
netmask 255.255.255.0
broadcast 192.168.0.255

Then edit the hosts file

$ sudo vim /etc/hosts

127.0.0.1 localhost
192.168.0.100 bioch6054
192.168.0.1 node01
192.168.0.2 node02
192.168.0.3 node03
..

And finally edit/check the hostname is consistent.

$ sudo vim /etc/hostname

A reboot here wouldn't go amiss. Now we can setup the headnode to use DHCP/NAT. First, find out the name of the DNS servers that the headnode has obtained via DHCP and make a note of these.

$ cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 129.68.1.150
nameserver 163.1.4.1
nameserver 129.67.71.244
search bioch.ox.ac.uk

Edit

$ sudo vim /etc/sysctl.conf

so this line is uncommented

net.ipv4.ip_forward=1

Save the file and run the following command to make the change effective without a reboot.

$ sudo sysctl -w net.ipv4.ip_forward=1

Now edit /etc/rc.local

$ sudo vim /etc/rc.local

Make sure the following two lines appear before the exit 0 line in the file.

/sbin/iptables -P FORWARD ACCEPT
/sbin/iptables --table nat -A POSTROUTING -o eth0 -j MASQUERADE

To make these iptables rules active without rebooting, run the following commands:

$ sudo iptables -P FORWARD ACCEPT
$ sudo iptables –-table nat -A POSTROUTING -o eth0 -j MASQUERADE

Now install the DHCP server

$ sudo apt-get install isc-dhcp-server

and edit its configuration file

$ sudo vim /etc/dhcp/dhcpd.conf

These are the bits of the file I changed. First need to add the DNS servers

option domain-name-servers 129.67.1.180, 163.1.2.1, 129.67.72.244;

Mark this as the authoritative server

authoritative;

And define the subnet

subnet 192.168.0.0 netmask 255.255.255.0 {
option routers 192.168.0.100;
option subnet-mask 255.255.255.0;
option broadcast-address 192.168.0.255;
group {
    host node01 { hardware ethernet 00:24:35:f3:db:86; fixed-address 192.168.0.1; }
    host node02 { hardware ethernet 00:24:35:f3:df:7e; fixed-address 192.168.0.2; }
    host node03 { hardware ethernet 00:24:35:f3:dc:74; fixed-address 192.168.0.3; }
    }
}

The bit in the group is important; here each compute node is identified by its MAC address and assigned a fixed IP address. The Xserves have a handy pull-out card at the back that tells you the MAC address of each ethernet port so there is no need to go digging around with ifconfig. Finally, tell DHCP that requests will come via the second ethernet port. Edit /etc/default/isc-dhcp-server so it reads

INTERFACES="eth1"

Now we can setup NFS. What caught me out here is Ubuntu 12.04 installs NFS version 4 by default but nearly all the guides I found were version 3. It sort of worked if you do this, but nodes would start to become unresponsive. Here we shall specify that we are using NFS version 3. On the headnode, the directories that are being exported must exist - we've already created /opt/software above and /home already exists. Install the NFS server

$ sudo apt-get install nfs-kernel-server

Now we add the following lines to /etc/exports

/home 192.168.0.0/24(rw,sync,no_subtree_check,no_root_squash)
/opt/software 192.168.0.0/24(rw,sync,no_subtree_check,no_root_squash)

and edit the server settings

$ sudo vim /etc/default/nfs-kernel-server

making sure the line reads

RPCMOUNTDOPTS="--manage-gids --no-nfs-version 4"

and start the service

$ sudo service nfs-kernel-server start

Compute nodes

Don't need to change the /etc/network/interfaces file as using DHCP and this will be setup by default to be.

# The loopback network interface
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp

Instead copy the /etc/hosts file over (but remember that the loopback address, 127.0.1.1, needs to refer the name of the compute node, not the headnode), and edit the hostname. Again we've already created the mount points, so add the following lines to /etc/fstab to ensure the directories are mounted at boot

bioch6054:/home /home nfs nfsvers=3
bioch6054:/opt/software /opt/software nfs nfsvers=3

A reboot here seems to do the trick. Now we have /home/fowler shared via NFS between all the machines we can setup an ssh keypair for logging without passwords.

$ ssh-keygen -t rsa
$ cd .ssh/
$ cat id_rsa.pub >> authorized_keys

You now should have a networked cluster. Each compute node should pick up a static IP from the headnode, everything should be reachable by a consistent naming scheme (e.g. ssh node02). Files written to the shared directories /home/ and /opt/software should be visible on all machines. The user fowler should be able to write and read files in /home/fowler on any machine and you should have sudo access to any file in the shared directories.

3. Compile GROMACS

We could simply compile GROMACS in the usual way, but install it into /opt/software. There are several problems with this, the main being that it will be dynamically linked to various compiler libraries on the headnode and so the binary won't work on the compute nodes (unless we also install the same compiler there too). Instead we shall use environment modules, which has already been installed on all machines. There are also some good walk-throughs here and here. Roughly speaking, we shall the pre-requisites for GROMACS, including gcc, into /opt/software and then setup 'module files' for each so that we can dynamically alter what is in our $PATH. This makes maintaining multiple versions of code (or the same version compiled using different compilers) much more straightforward and is the approach commonly taken by large computing clusters but it can be useful on a single workstation. The environment modules package has been installed in /usr/share/modules First we need to alter where module looks for modulefiles (the $MODULEPATH).

$ sudo vim /usr/share/modules/init/.modulespath

Comment out all the lines and add this to the end

/opt/software/modules

Now we can activate modules by loading this shell script

$ source /etc/profile.d/modules.sh

The structure I have adopted is to download and compile code in

/opt/software/src

and then install it in

/opt/software/apps

with modulesfiles located in

/opt/software/modules

Compiling gcc

The first module to compile is a compiler. We'll need subversion and also flex. This is all still on the headnode

$ sudo apt-get install subversion flex

First, let's check what versions are

$ svn ls svn://gcc.gnu.org/svn/gcc/tags | grep gcc | grep release

The latest stable version is 5.3.0 so let's install that one.

/opt/software/src/gcc/$ sudo svn co svn://gcc.gnu.org/svn/gcc/tags/gcc_5_3_0_release/
/opt/software/src/gcc/$ cd gcc_5_3_0_release/

Download the prerequisites and make a build directory

/opt/software/src/gcc/gcc_5_3_0_release/$ sudo ./contrib/download_prerequisites
/opt/software/src/gcc/gcc_5_3_0_release/$ sudo mkdir build
/opt/software/src/gcc/gcc_5_3_0_release/$ cd build
/opt/software/src/gcc/gcc_5_3_0_release/build$ sudo ../configure --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/opt/software/apps/gcc/5.3.0 --disable-multilib
/opt/software/src/gcc/gcc_5_3_0_release/build/$ make -j 16
/opt/software/src/gcc/gcc_5_3_0_release/build/$ sudo make install

The make command will take up to an hour. Change the number after the -j flag to match the number of cores on your machine. If this works then we should now have two versions of gcc installed; 4.8.3 from build-essential (/usr/bin/gcc) that is in our $PATH by default and now 5.3.0

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

$ /opt/software/apps/gcc/5.3.0/bin/gcc --version
gcc (GCC) 5.3.0

Now we need to make a module file

$ cd /opt/software/modules
/opt/software/modules/$ sudo mkdir gcc
/opt/software/modules/$ cd gcc
/opt/software/modules/gcc/$ sudo touch 5.3.0
/opt/software/modules/gcc/$ sudo vim 5.3.0

and mine looks something like this

#%Module1.0#####################################################################
##
## Application module file
##
proc ModulesHelp { } {
global version
global app
puts stderr "\tAdds `app-$version' to your PATH environment variable and necessary libraries"
}

set app gcc
set version 5.3.0

module-whatis "loads the necessary `$app-$version' library paths"

set BASEPATH /opt/software/apps/$app/$version/

setenv $app $BASEPATH
setenv GCCDIR $BASEPATH

prepend-path PATH $BASEPATH/bin/
prepend-path LD_LIBRARY_PATH $BASEPATH/lib:$BASEPATH/lib64
prepend-path MANPATH $BASEPATH/man/

prepend-path --delim " " CPPFLAGS " -I$BASEPATH/include"
prepend-path --delim " " CFLAGS " -I$BASEPATH/include"
prepend-path --delim " " LDFLAGS " -L$BASEPATH/lib/ -L$BASEPATH/lib64/"

setenv cc gcc
setenv cxx gcc
setenv f77 gfortran
setenv f90 gfortran
setenv f95 gfortran
setenv fc gfortran

setenv CC gcc
setenv CXX gfortran
setenv F77 gfortran
setenv F90 gfortran
setenv F95 gfortran
setenv FC gfortran

Now if we ask what modules are available, we should find there is one

$ module avail

---------------------------- /opt/software/modules -----------------------------
gcc/5.3.0

And no modules are loaded

$ module list
No Modulefiles Currently Loaded.

At the moment the system gcc installed when we ran sudo apt-get install build-essential should be in our $PATH

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

If we load the gcc module (no need to specify which version as there is only one at present)

$ module load gcc
$ module list
Currently Loaded Modulefiles:
1) gcc/5.3.0
$ gcc --version
gcc (GCC) 5.3.0

Then you can see the gcc compiler we have just installed is now the active one!

Compiling Open-MPI

Next let's compile open-mpi. The latest stable version is 1.10.2

$ cd /opt/software/src
/opt/software/src/$ sudo mkdir openmpi
/opt/software/src/$ cd openmpi
/opt/software/src/openmpi/$ wget https://www.open-mpi.org/software/ompi/v1.10/downloads/openmpi-1.10.2.tar.gz
/opt/software/src/openmpi/$ tar zxvf openmpi-1.10.2.tar.gz
/opt/software/src/openmpi/$ cd openmpi-1.10.2
/opt/software/src/openmpi/$ sudo mkdir build-gcc53
/opt/software/src/openmpi/$ cd build-gcc53
/opt/software/src/openmpi/build-gcc53$ sudo ../configure --prefix=/opt/software/apps/openmpi/1.10.2/ --enable-shared=yes --enable-static=yes --with-slurm
/opt/software/src/openmpi/build-gcc53$ sudo make -j 16
/opt/software/src/openmpi/build-gcc53$ sudo make install

Then create a modules file

$ cd /opt/software/modules
/opt/software/modules/$ sudo mkdir openmpi
/opt/software/modules/$ cd openmpi
/opt/software/modules/openmpi/$ sudo vim 1.10.2

Mine looks like

#%Module1.0#####################################################################
##
## openmpi modulefile
##
##
proc ModulesHelp { } {
global version

puts stderr "\tAdds 64-bit OpenMPI compiled with GCC to your environment."
puts stderr "\tDirectory: $root"
}

module-whatis "adds OpenMPI to your environment variables"

set app openmpi
set version 1.10.2
set root /opt/software/apps/openmpi/1.10.2

prepend-path PATH $root/bin
prepend-path LD_RUN_PATH $root/lib
prepend-path LD_LIBRARY_PATH $root/lib
prepend-path MANPATH $root/share/man

append-path -d " " CPPFLAGS -I$root/include
append-path -d " " LDFLAGS -L$root/lib

setenv MPI_HOME $root
setenv MPIRUN $root/bin/mpirun

Now we have two modules

$ module avail
---------------------------- /opt/software/modules -----------------------------
gcc/5.3.0 openmpi/1.10.2

### Compiling GROMACS Now, at last, we can tackle GROMACS. I could have also put FFTW into modules, but GROMACS is quite happy downloading it and compiling FFTW itself so I haven't bothered. Also if we were using GPUs, I should have installed CUDA into my modules, but it looks like the GPU cards I can fit in the limited space inside an Xserve are not powerful enough, so I'm going to ignore that too for the present. First, let's download and unpack GROMACS

$ cd /opt/software/src
/opt/software/src/$ sudo mkdir gromacs
/opt/software/src/$ cd gromacs
/opt/software/src/gromacs/$ wget ftp://ftp.gromacs.org/pub/gromacs/gromacs-5.1.2.tar.gz
/opt/software/src/gromacs/$ tar zxvf gromacs-5.1.2.tar.gz
/opt/software/src/gromacs/$ cd gromacs-5.1.2
/opt/software/src/gromacs/gromacs-5.1.2/$ sudo vim install.sh

In this shell script (install.sh) I'm going to record all the options I gave to GROMACS. This is useful since they are a bit more complex and I'm likely to compile GROMACS fairly frequently. This is one I have adapted from my old lab and first compiles "normal" GROMACS and then the MPI version of mdrun.

#! /bin/bash

GROMACS_SRC='/opt/software/src/gromacs/gromacs-5.1.2'
INSTALL_DIR='/opt/software/apps/gromacs/5.1.2'

source /etc/profile.d/modules.sh
module purge
module load openmpi/1.10.2
module load gcc/5.3.0

mkdir $GROMACS_SRC/build-gcc53
cd $GROMACS_SRC/build-gcc53

echo $PWD

cmake $GROMACS_SRC \
-DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \
-DGMX_MPI=OFF \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER=g++ \
-DGMX_BUILD_MDRUN_ONLY=OFF \
-DGMX_BUILD_OWN_FFTW=ON
-DGMX_X11=OFF \
-DGMX_GPU=OFF \
-DGMX_DEFAULT_SUFFIX=OFF \
-DGMX_CPU_ACCELERATION=AVX \
-DBUILD_SHARED_LIBS=OFF \
-DGMX_EXTERNAL_BLAS=OFF \
-DGMX_EXTERNAL_LAPACK=OFF

make -j 16

make install

cd ..

mkdir $GROMACS_SRC/build-gcc53-openmpi
cd $GROMACS_SRC/build-gcc53-openmpi

cmake $GROMACS_SRC \
-DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \
-DGMX_MPI=ON \
-DCMAKE_C_COMPILER=mpicc \
-DCMAKE_CXX_COMPILER=mpicxx \
-DGMX_BUILD_MDRUN_ONLY=ON \
-DGMX_BUILD_OWN_FFTW=ON \
-DGMX_X11=OFF \
-DGMX_GPU=OFF \
-DGMX_DEFAULT_SUFFIX=ON \
-DBUILD_SHARED_LIBS=OFF \
-DGMX_EXTERNAL_BLAS=OFF \
-DGMX_EXTERNAL_LAPACK=OFF

make -j 16

make install

Then we can run this as root

/opt/software/src/gromacs/gromacs-5.1.2/$ sudo ./install.sh

and wait. Then create a module file

$ cd /opt/software/modulefiles
/opt/software/modulefiles/$ sudo mkdir gromacs
/opt/software/modulefiles/$ cd gromacs
/opt/software/modulefiles/gromacs/$ sudo vim 5.1.2

which looks like

#%Module1.0#####################################################################
##
## gromacs Modulefile
##
##
proc ModulesHelp { } {
global version

puts stderr "\tAdds 64-bit GROMACS compiled with GCC to your environment."
puts stderr "\tDirectory: $root"
}

module-whatis "adds GROMACS to your environment variables"

if {! [ is-loaded openmpi/1.10.2 ] } {
module load openmpi/1.10.2
}
prereq openmpi/1.10.2

if {! [ is-loaded gcc/5.3.0 ] } {
module load gcc/5.3.0
}
prereq gcc/5.3.0

set version 5.1.2
set app gromacs
set root /opt/software/apps/gromacs/${version}

prepend-path PATH $root/bin
prepend-path LD_RUN_PATH $root/lib
prepend-path LD_LIBRARY_PATH $root/lib
prepend-path MANPATH $root/share/man

setenv GMXDIR $root
setenv GMXBIN $root/bin
setenv GMXLDLIB $root/lib
setenv GMXMAN $root/share/man
setenv GMXDATA $root/share
setenv GMXFONT 10x20

So now

$ module avail
---------------------------- /opt/software/modules -----------------------------
gcc/5.3.0 gromacs/5.1.2 openmpi/1.10.2

Let's try it on a compute node

$ ssh node01
$ module add gromacs
$ gmx -h
:-) GROMACS - gmx, VERSION 5.1.2 (-:

GROMACS is written by:
Emile Apol Rossen Apostolov Herman J.C. Berendsen Par Bjelkmar

Hurray it works!

4. Installing SLURM

As I wrote before, this was the part that scared me most, but turned out to be the easiest. We've already installed SLURM using apt-get on all machines; this has also installed MUNGE which is needed for authentication. The first step is to setup MUNGE on all machines. First we need to create a MUNGE key on the headnode and distribute it to all the machines for authentication.

$ sudo /usr/sbin/create-munge-key

To copy it to the compute nodes, the easiest thing is to put a copy in your user home directory as this will be shared by NFS.

$ sudo cp /etc/munge/munge.key /home/fowler/

Then on each compute node we can copy the file into the right directory

/home/fowler/$ sudo cp munge.key /etc/munge/

There is a problem with permissions here; the original key is owned by munge which is part of the munge group. As we've been moving it around with sudo, it is now owned by root. So we also need to change the permissions back on each compute node.

$ sudo chown munge /etc/munge/munge.key
$ sudo chgrp munge /etc/munge/munge.key

There appears to be a bug with Ubuntu as well, this is fixed by adding the following line, on all machines, to /etc/default/munge

OPTIONS="--force"

Now on each machine we can start the MUNGE service

$ sudo service munge start

and check it is running

$ ps -e | grep munge

Now we are in a position to setup SLURM.

/home/fowler$ cp /usr/share/doc/slurm-llnl/examples/slurm.conf.simple.gz .
/home/fowler$ gunzip slurm.conf.simple.gz
/home/fowler$ mv slurm.conf.simple slurm.conf
/home/fowler$ vim slurm.conf

This is an example, basic SLURM configuration file which you can just alter. All I did was change the following lines from their defaults to match my cluster, which is currently

ControlMachine=bioch6054
..
NodeName=node0[1-2] Procs=16 State=UNKNOWN
PartitionName=production Nodes=node0[1-2] Default=YES MaxTime=INFINITE State=UP

This file needs to be copied to every machine, including the headnode.

/home/fowler/$ sudo cp slurm.conf /etc/slurm

Now we can start the SLURM controller on the headnode

$ sudo service slurm-llnl start

and the SLURM daemon on each compute note

$ slurrmd -c

For a simple check, run the following on the headnode

$ srun -N1 hostname
node01

Or to try using two nodes

$ srun -N2 hostname
node01
node02

I'm going to assume you have a GROMACS TPR file called at-1-1.tpr that you've made using GROMPP. Let's benchmark the cluster. So on the headnode, create and edit a SLURM job file

/home/fowler$ sudo vim at-1-1.sh

Mine looks like

#!/bin/bash
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=1
#SBATCH --time=00:15:00
#SBATCH --job-name=at-1-1

source /etc/profile.d/modules.sh
module load gromacs/5.1.2

gmx mdrun -deffnm at-1-1 -ntmpi 1 -ntomp 1 -maxh 0.1 -resethway -noconfout -stepout 100 -v

This has some benchmarking specific flags. Make sure at-1-1.sh and at-1-1.tpr are both present, then submit it to the queue

$ sbatch at-1-1.sh
Submitted batch job 1

and then we can check the queue

$ squeue
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1 productio at-1-1 fowler R 0:28 1 node02

Or, let's run an MPI GROMACS job instead.

#!/bin/bash
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=8
#SBATCH --cpus-per-task=1
#SBATCH --time=00:15:00
#SBATCH --job-name=at-1-1

source /etc/profile.d/modules.sh
module load gromacs/5.1.2

mpirun -np 8 mdrun_mpi -deffnm at-1-1 -maxh 0.1 -resethway -noconfout -stepout 100 -v

Job done!