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?
- Viele Dateien sind mehr als 10-20k in einem Verzeichnis, und Millionen in Unterverzeichnissen (Bauchgefühl)
- Grosse Dateien sind mehr als 1GB
- Kleine Dateien sind <100kB
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:
- UL HPC ← VERY CONCISE RESOURCE
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:
- out.sh : the actual program doing the calculations, takes here 2 arguments like
out.sh 2 222323
- slurm.batch : the actual job script to do the calculations
- params.list : list of parameters
- collect.batch : a sbatch script to collect the created data
- submit.sh : a job submission script
–mem-per-cpu
setting forsrun
is crucial!
- 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.
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.
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.
- create a key:
ssh-keygen -t ed25519
- 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!
- 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
- now login to protein.cluster, it may ask for the passphrase.