Table of Contents

Tips/Tricks

Profiling C Programs

You can profile programs with ''valgrind'' and analyze the output file with kcachegrind.

valgrind --tool=callgrind ./out

Daten erzeugen

Die Performance, besonders über das Netzwerk hängt besonders ab von Dateizugriffen.

Was sind denn “viele” Dateien, “grosse” und “kleine” Dateien?

Generelle Punkte

Gross ist besser

Lieber wenige grosse Dateien als viele kleine Dateien, grose Dateien erzeugen weniger IOPS, das belastet das Netzwerk und die SSDs nicht so stark. Nicht vergessen, es könnten noch viele andere ebenfalls auf das gleiche Dateisystem zugreifen.

Minimiere Dateioperationen

Teure Operationen wie flush, open, close nicht in inneren Schleifen verwenden:

// NEGATIVES BEISPIEL
main()
{
    for(int i = 0; i <= 300; i += 20)
    {
        f=open(.., 'a');
        f.write(..);
        f.flush(..); // besonders teuer: Daten werden hier auf Platte geschrieben, vorher sind die noch im Cache. Nach dem flush ist
        f.close(..);
    }
}
// POSITIVES BEISPIEL
 
{
    f=open(.., 'a');
    for(int i = 0; i <= 300; i += 20)
    {
 
        f.write(..);
        if (i%100 ==0) f.flush(..); // flush nur nach jedem 100ten Wert
 
    }
    f.close(..);
}
Nicht ASCII benutzen

ASCII Text ist sehr unpraktisch und sehr verschwenderisch bezgl. des Platzverbrauchs, es lässt sich idR auf 20% komprimieren. Je nachdem was man hat kann man z.B. Numpy Arrays binär speichern, z.B. mit Pickle, und dann direkt einlesen ohne zu parsen.

from tempfile import TemporaryFile
outfile = TemporaryFile()
x = np.arange(10)
np.save(outfile, x) # oder savez für Komprimierung.
y=np.load(outfile)

Auch C Arrays kann man binär Speichern und einlesen:

FILE *f = fopen("client.data", "wb");
fwrite(clientdata, sizeof(char), sizeof(clientdata), f);
fclose(f);

HDF5 komprimiert z.B. wenn man das einstellt, man kann auch eine fixe Genauigkeit angeben etc. Zudem kann man den Datasets Attribute/Metadaten zuweisen wie z.B. Parameter die zur Erzeugung verwendet wurden. HDF5 ist relativ komplex aber machbar, die Vorteile sind auf jedenfall nicht einfach von der Hand zu weisen. Die Dokumentation ist sehr ausgiebig, man kommt recht schnell zu einem Ergebnis. In der AG Vogel wechseln wir zwischen C und Python für den Zugriff. Mit hdf-compass können HDF Dateien angezeigt werden, Tabellen können geplotted werden und man kann Daten exportieren. Für Python gibt es das https://docs.h5py.org/en/stable/quick.html|h5py Modul.

Szenario 1: sequentiell

Programm erzeugt 1024 Zeilen x 6 Spalten Arrays, und das 1000 mal bei einem Aufruf.

Dafür kann man z.B hdf5 nutzen und jedes Array in einer Baumstruktur abspeichern:

(Es gibt Tools zum anzeigen der Dateien)

HDF5 Bibliotheken machen es einfach z.B. über alle Arrays zu laufen und auszuwerten. Es gibt natürlich noch andere Dateiformate, aber HDF5 ist mir geläufig da wir das für unsere NMR Messungen nutzen. Es sind mit pytables und h5py auch sehr stabile Python Bibliotheken vorhanden. h5py sollte in Anaconda drin sein.

Szenario 2: parallel

Programm erzeugt ein 1024 Zeilen x 6 Spalten Array, das Programm wird 1000 mal aufgerufen, auf unterscheidlichsten Rechnern.

Paralleles schreiben in eine einzige Datei ist fast nicht möglich (Locking, etc..). Deswegen bleibt nur der Weg über einzelne Dateien. Was man aber machen kann ist nach dem Durchlauf die Dateien in HDF umzuwandeln und zu bündeln.

Beispielcode um viele ASCII Dateien in eine HDF5 Datei zu packen (./create_h5.py params.0?):

create_h5.py
#!/usr/bin/env python3
import h5py
from numpy import loadtxt
from sys import argv
 
with h5py.File("collect.h5","w") as h5:
    for afile in argv[1:]:
        print(afile)
        data = loadtxt(afile)
        # here is the data type an intt64, could be also Float64 (f), int32 (i8), etc. 
        dataset = h5.create_dataset(f"{afile}", data=data, compression='gzip', shuffle=True, dtype="i") 
        # example for an attribute for this dataset (the original filename)
        dataset.attrs["filename"] = afile
 
h5.close()
# do not forget to delete the files

Bei vielen (>1000) kleinen Dateien bietet es sich an nur das lokale (scratch) Dateisystem zu benutzen, nicht ein Netzlaufwerk. Das macht aber leider die Sammlung wieder komplexer. Ein Vorschlag der Admins wäre die Dateien des Jobs mit tar zu packen (tar cfz files.tar.gz files*.dat), dann diese Dateien von allen Knoten auf einem Konten entpacken und zusammenführen.

SLURM Job Submission

References:

Please limit the number of submitted jobs, do not use job arrays for massive parameter sweeps. This will overwhelm the SLURM scheduler.

Don't submit thousands of jobs!

Job Submission with GNU parallel

Some tips fot submitting embarrassingly parallel jobs more efficiently.

This needs some more testing! I am not convinced that srun has to be used after the parallel command. The examples I found on the internet all used it, but that job step will still be recorded in the slurmdbd.

You need several part for this:

slurm.batch
#!/bin/bash
#SBATCH --mail-type=NONE
#SBATCH --nodes=1
#SBATCH --ntasks=16
 
# parallel options
# -P,j,--jobs N     Number of jobslots on each machine. Run up to N jobs in parallel.  0 means as many as possible.
# -a input-file Use input-file as input source. 
#   If multiple -a are given, each input-file will be treated as an input source, and all combinations of input sources will be generated. 
#    E.g. The file foo contains 1 2, the file bar contains a b c.  -a foo -a bar will result in the combinations (1,a) (1,b) (1,c) (2,a) (2,b) (2,c). This is useful for replacing nested for-loops.
# --colsep Column separator. The input will be treated as a table with regexp separating the columns. The n'th column can be access using {n} or {n.}. E.g. {3} is the 3rd column.
 
parallel --delay=0.1 --jobs=$SLURM_NTASKS --arg-file=params.list --colsep=" " \
	srun --mem-per-cpu=2000 --exclusive --ntasks=1 --nodes=1 bash out.sh {1} {2}
params.list
6 10064217
8 55066712
10 53991502
9 25649130
9 31032513
9 74063598
6 32381514
7 31916727
7 27385465
6 43468477
6 93701606
7 75175606
9 51106571
8 84389210
...
collect.batch
#!/bin/bash
#SBATCH --mail-type=NONE
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem-per-cpu=4000
 
## postprocessing the data a bit and cleanup
tar cvfz mydata.tar.gz output_*.dat
rm -v output_*.dat
submit.sh
#!/bin/bash
 
if [ $? -ne 0 ]; then
    echo "sbatch failed"
    exit 1
fi
 
# first job
SBATCH_OUTPUT=$(sbatch $1)
PARALLEL_JOB=$(echo $SBATCH_OUTPUT | grep -oE "[0-9]+")
echo $PARALLEL_JOB
 
# second job
sbatch --dependency=afterok:$PARALLEL_JOB collect.batch

The submit script extracts the jobif of the slurm.batch job and adds that jobid as a dependency for the collect.batch script. Only if the first job was succesfull and all runs are finished will the secodn job collect the all the data.

The actual slurm.batch script will run 16 processes simultaniously, it will loop through the params.list files with possibly a lot of parameters. Only when all parameters are consumed will the job finish and the collect.bastch job will start. agdrossel:diagram1.png

If you have a huge parameter list, you can create the list, split the list in parts (man split) and submit each part as a separate job. If it is a text file, you can use the shell command:

split --verbose -n l/4 --numeric-suffixes=1  params.list params.

to split the file in 4 parts named params.01 to params.04.

You can then submit multiple jobs with the splitted files. The call graph looks something akin to the following.

agdrossel:diagram2.png

The corresponding submit script looks like this:

submit_parts.sh
#!/bin/bash
if [ $# -le 2 ]; then
    echo "sbatch failed: no enough input files"
    exit 1
fi
 
PARALLEL_JOB_LIST=()
 
# first argument is the job file
# second and more are the parameter files
for PARAM_FILE in "${@:2}"; do
	SBATCH_OUTPUT=$(sbatch $1 $PARAM_FILE)
	PARALLEL_JOB_LIST+=($(echo $SBATCH_OUTPUT | grep -oE "[0-9]+"))
	echo "${SBATCH_OUTPUT}"
done
 
# collect the data after all jobs are done
# some bash magic to concatenate the jobids seperated with ","
 
IFS="," 
sbatch --dependency=afterok:"${PARALLEL_JOB_LIST[*]}" collect.batch

We need to modify the slurm.batch script to take a part of the splitted parameter list as an argument:

slurm_part.batch
#!/bin/bash
#SBATCH --mail-type=NONE
#SBATCH --ntasks=16
 
PARAMS=$1
parallel --delay=0.1 --jobs=$SLURM_NTASKS --arg-file=$PARAMS --colsep=" " \
	srun --mem-per-cpu=2000 --exclusive --ntasks=1 --nodes=1 bash out.sh {1} {2}

You can run it like this:

bash submit_parts.sh slurm_part.batch params.0? # the ? matches any single character.

Another possible way is the –multi-prog parameter for srun. As an example you can use this document.

One can let jobs wait for each other also with the -d, –dependency=singleton parameter. This tells the job to begin execution after any previously launched jobs sharing the same job name and user has terminated. Job name is set with -J parameter.

# Abstract search space parameters
min=1
max=2000
chunksize=200
for i in $(seq $min $chunksize $max); do
    ${CMD_PREFIX} sbatch \
                  -J ${JOBNAME}_$(($i/$chunksize%${MAXNODES})) --dependency singleton \
                  ${LAUNCHER} --joblog log/state.${i}.parallel.log  "{$i..$((i+$chunksize))}";
done

Tools

These are tools that exist, if requested we will try and make them available on the cluster:

Group Specific

AG Drossel

AG Liebchen

AG Vogel

The head node protein does not allow password logins, you need to use ssh keys.

  1. create a key: ssh-keygen -t ed25519
  2. We admins stronlgy recommend to use a very strong passphrase. Together with ssh-agent you have to type it only once per login to your desktop!
  3. add the public part to the authorized_keys file and set correct premissions: cat .ssh/id_ed25519.pub | tee -a .ssh/authorized_keys && chmod 0600 .ssh/authorized_keys
  4. now login to protein.cluster, it may ask for the passphrase.

SSH Agent

The ssh-agent should be startet automatically on login, Cinnamon for example will show a screen upon login to the desktop. If not you need to set the GNOME Keyring SSH Agent to start automatically: