bscpkgs/garlic/doc/ug.ms
2023-03-02 10:38:43 +01:00

1692 lines
55 KiB
Plaintext

.\" Point size fails when rending html in the code blocks
.\".nr PS 11p
.nr GROWPS 3
.nr PSINCR 2p
.fam P
.\" ===================================================================
.\" Some useful macros
.\" ===================================================================
.\"
.\" Code start (CS) and end (CE) blocks
.de CS
.DS L
\fC
..
.de CE
\fP
.DE
..
.\" Code inline:
.\" .CI "inline code"
.de CI
\fC\\$1\fP\\$2
..
.\" ===================================================================
.\" \&
.\" .sp 3c
.\" .LG
.\" .LG
.\" .LG
.\" .LG
.\" Garlic: User guide
.\" .br
.\" .NL
.\" Rodrigo Arias Mallo
.\" .br
.\" .I "Barcelona Supercomputing Center"
.\" .br
.\" \*[curdate]
.\" .sp 17c
.\" .DE
.\" .CI \*[gitcommit]
.TL
Garlic: User Guide
.AU
Rodrigo Arias Mallo
.AI
Barcelona Supercomputing Center
.AB
.LP
This document contains all the information to configure and use the
garlic benchmark. All stages from the development to the publication
are covered, as well as the introductory steps required to setup the
machines.
.DS L
.SM
\fC
Generated on \*[curdate]
Git commit: \*[gitcommit]
\fP
.DE
.AE
.\" ===================================================================
.NH 1
Introduction
.LP
The garlic framework is designed to fulfill all the requirements of an
experimenter in all the steps up to publication. The experience gained
while using it suggests that we move along three stages despicted in the
following diagram:
.DS L
.SM
.PS 5
linewid=1.4;
arcrad=1;
right
S: box "Source" "code"
line "Development" invis
P: box "Program"
line "Experimentation" invis
R:box "Results"
line "Data" "exploration" invis
F:box "Figures"
# Creates a "cycle" around two boxes
define cycle {
arc cw from 1/2 of the way between $1.n and $1.ne \
to 1/2 of the way between $2.nw and $2.n ->;
arc cw from 1/2 of the way between $2.s and $2.sw \
to 1/2 of the way between $1.se and $1.s ->;
}
cycle(S, P)
cycle(P, R)
cycle(R, F)
.PE
.DE
In the development phase the experimenter changes the source code in
order to introduce new features or fix bugs. Once the program is
considered functional, the next phase is the experimentation, where
several experiment configurations are tested to evaluate the program. It
is common that some problems are spotted during this phase, which lead
the experimenter to go back to the development phase and change the
source code.
.PP
Finally, when the experiment is considered completed, the
experimenter moves to the next phase, which envolves the exploration of
the data generated by the experiment. During this phase, it is common to
generate results in the form of plots or tables which provide a clear
insight in those quantities of interest. It is also common that after
looking at the figures, some changes in the experiment configuration
need to be introduced (or even in the source code of the program).
.PP
Therefore, the experimenter may move forward and backwards along three
phases several times. The garlic framework provides support for all the
three stages (with different degrees of madurity).
.\" ===================================================================
.NH 2
Machines and clusters
.LP
Our current setup employs multiple machines to build and execute the
experiments. Each cluster and node has it's own name and will be
different in other clusters. Therefore, instead of using the names of
the machines we use machine classes to generalize our setup. Those
machine clases currently correspond to a physical machine each:
.IP \(bu 12p
.B Builder
(xeon07): runs the nix-daemon and performs the builds in /nix. Requires
root access to setup the
.I nix-daemon
with multiple users.
.IP \(bu
.B Target
(MareNostrum 4 compute nodes): the nodes where the experiments
are executed. It doesn't need to have /nix installed or root access.
.IP \(bu
.B Login
(MareNostrum 4 login nodes): used to allocate resources and run jobs. It
doesn't need to have /nix installed or root access.
.IP \(bu
.B Laptop
(where the keyboard is attached, can be anything): used to connect to the other machines.
No root access is required or /nix, but needs to be able to connect to
the builder.
.LP
The machines don't need to be different of each others, as one machine
can implement several classes. For example the laptop can act as the
builder too but is not recommended. Or the login machine can also
perform the builds, but is not possible yet in our setup.
.\" ===================================================================
.NH 2
Reproducibility
.LP
An effort to facilitate the reproducibility of the experiments has been
done, with varying degrees of success. The names of the different levels
of reproducibility have not been yet standarized, so we define our own
to avoid any confusion. We define three levels of reproducibility based
on the people and the machine involved:
.IP \(bu 12p
R0: The \fIsame\fP people on the \fIsame\fP machine obtain the same result
.IP \(bu
R1: \fIDifferent\fP people on the \fIsame\fP machine obtain the same result
.IP \(bu
R2: \fIDifferent\fP people on a \fIdifferent\fP machine obtain the same result
.LP
The garlic framework distinguishes two types of results: the result of
\fIbuilding a derivation\fP (usually building a binary or a library from the
sources) and the results of the \fIexecution of an experiment\fP (typically
those are the measurements performed during the execution of the program
of study).
.PP
For those two types, the meaning of
.I "same result"
is different. In the case of building a binary, we define the same
result if it is bit-by-bit identical. In the packages provided by nixos
is usually the case except some rare cases. One example is that during the build process,
a directory is listed by the order of the inodes, giving a random order
which is different between builds. These problems are tracked by the
.URL https://r13y.com/ r13y
project. About 99% of the derivations of the minimal package set achieve
the R2 property.
.PP
On the other hand, the results of the experiments are always bit-by-bit
different. So we change the definition to state that they are the same
if the conclusions that can be obtained are the same. In particular, we
assume that the results are within the confidence interval. With this
definition, all experiments are currently R1. The reproducibility level
R2 is not posible yet as the software is compiled to support only the
target machine, with an specific interconnection.
.\" ===================================================================
.bp
.NH 1
Preliminary steps
.LP
The peculiarities of our setup require that users perform some actions
to use the garlic framework. The content of this section is only
intended for the users of our machines, but can serve as reference in
other machines.
.PP
The names of the machine classes are used in the command line prompt
instead of the actual name of the machine, to indicate that the command
needs to be executed in the stated machine class, for example:
.CS
builder% echo hi
hi
.CE
When the machine class is not important, it is ignored and only the
.CI "%"
prompt appears.
.\" ===================================================================
.NH 2
Configure your laptop
.LP
To easily connect to the builder (xeon07) in one step, configure the SSH
client to perform a jump over the Cobi login node. The
.I ProxyJump
directive is only available in version 7.3 and upwards. Add the
following lines in the
.CI \(ti/.ssh/config
file of your laptop:
.CS
Host cobi
HostName ssflogin.bsc.es
User your-username-here
Host xeon07
ProxyJump cobi
HostName xeon07
User your-username-here
.CE
You should be able to connect to the builder typing:
.CS
laptop$ ssh xeon07
.CE
To spot any problems try with the
.CI -v
option to enable verbose output.
.\" ===================================================================
.NH 2
Configure the builder (xeon07)
.LP
In order to use nix you would need to be able to download the sources
from Internet. Usually the download requires the ports 22, 80 and 443
to be open for outgoing traffic.
.PP
Check that you have network access in
xeon07 provided by the environment variables \fIhttp_proxy\fP and
\fIhttps_proxy\fP. Try to fetch a webpage with curl, to ensure the proxy
is working:
.CS
xeon07$ curl x.com
x
.CE
.\" ===================================================================
.NH 3
Create a new SSH key
.LP
There is one DSA key in your current home called "cluster" that is no
longer supported in recent SSH versions and should not be used. Before
removing it, create a new one without password protection leaving the
passphrase empty (in case that you don't have one already created) by
running:
.CS
xeon07$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (\(ti/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in \(ti/.ssh/id_rsa.
Your public key has been saved in \(ti/.ssh/id_rsa.pub.
\&...
.CE
By default it will create the public key at \f(CW\(ti/.ssh/id_rsa.pub\fP.
Then add the newly created key to the authorized keys, so you can
connect to other nodes of the Cobi cluster:
.CS
xeon07$ cat \(ti/.ssh/id_rsa.pub >> \(ti/.ssh/authorized_keys
.CE
Finally, delete the old "cluster" key:
.CS
xeon07$ rm \(ti/.ssh/cluster \(ti/.ssh/cluster.pub
.CE
And remove the section in the configuration \f(CW\(ti/.ssh/config\fP
where the key was assigned to be used in all hosts along with the
\f(CWStrictHostKeyChecking=no\fP option. Remove the following lines (if
they exist):
.CS
Host *
IdentityFile \(ti/.ssh/cluster
StrictHostKeyChecking=no
.CE
By default, the SSH client already searchs for a keypair called
\f(CW\(ti/.ssh/id_rsa\fP and \f(CW\(ti/.ssh/id_rsa.pub\fP, so there is
no need to manually specify them.
.PP
You should be able to access the login node with your new key by using:
.CS
xeon07$ ssh ssfhead
.CE
.\" ===================================================================
.NH 3
Authorize access to the repository
.LP
The sources of BSC packages are usually downloaded directly from the PM
git server, so you must be able to access all repositories without a
password prompt.
.PP
Most repositories are open to read for logged in users, but there are
some exceptions (for example the nanos6 repository) where you must have
explicitly granted read access.
.PP
Copy the contents of your public SSH key in \f(CW\(ti/.ssh/id_rsa.pub\fP
and paste it in GitLab at
.CS
https://pm.bsc.es/gitlab/profile/keys
.CE
Finally verify the SSH connection to the server works and you get a
greeting from the GitLab server with your username:
.CS
xeon07$ ssh git@bscpm03.bsc.es
PTY allocation request failed on channel 0
Welcome to GitLab, @rarias!
Connection to bscpm03.bsc.es closed.
.CE
Verify that you can access the nanos6 repository (otherwise you
first need to ask to be granted read access), at:
.CS
https://pm.bsc.es/gitlab/nanos6/nanos6
.CE
Finally, you should be able to download the nanos6 git
repository without any password interaction by running:
.CS
xeon07$ git clone git@bscpm03.bsc.es:nanos6/nanos6.git
.CE
Which will create the nanos6 directory.
.\" ===================================================================
.NH 3
Authorize access to MareNostrum 4
.LP
You will also need to access MareNostrum 4 from the xeon07 machine, in
order to run experiments. Add the following lines to the
\f(CW\(ti/.ssh/config\fP file and set your user name:
.CS
Host mn0 mn1 mn2
User <your user name in MN4>
.CE
Then copy your SSH key to MareNostrum 4 (it will ask you for your login
password):
.CS
xeon07$ ssh-copy-id -i \(ti/.ssh/id_rsa.pub mn1
.CE
Finally, ensure that you can connect without a password:
.CS
xeon07$ ssh mn1
\&...
login1$
.CE
.\" ===================================================================
.NH 3
Clone the bscpkgs repository
.LP
Once you have Internet and you have granted access to the PM GitLab
repositories you can begin building software with nix. First ensure
that the nix binaries are available from your shell in xeon07:
.CS
xeon07$ nix --version
nix (Nix) 2.3.6
.CE
Now you are ready to build and install packages with nix. Clone the
bscpkgs repository:
.CS
xeon07$ git clone git@bscpm03.bsc.es:rarias/bscpkgs.git
.CE
Nix looks in the current folder for a file named \f(CWdefault.nix\fP for
packages, so go to the bscpkgs directory:
.CS
xeon07$ cd bscpkgs
.CE
Now you should be able to build nanos6 (which is probably already
compiled):
.CS
xeon07$ nix-build -A bsc.nanos6
\&...
/nix/store/...2cm1ldx9smb552sf6r1-nanos6-2.4-6f10a32
.CE
The installation is placed in the nix store (with the path stated in
the last line of the build process), with the \f(CWresult\fP symbolic
link pointing to the same location:
.CS
xeon07$ readlink result
/nix/store/...2cm1ldx9smb552sf6r1-nanos6-2.4-6f10a32
.CE
.\" ###################################################################
.NH 3
Configure garlic
.LP
In order to launch experiments in the
.I target
machine, it is required to configure nix to allow a directory to be
available during the build process, where the results will be stored
before being copied in the nix store. Create a new
.CI garlic
directory in your
personal cache directory and copy the full path:
.CS
xeon07$ mkdir -p \(ti/.cache/garlic
xeon07$ readlink -f \(ti/.cache/garlic
/home/Computational/rarias/.cache/garlic
.CE
Then create the nix configuration directory (if it has not already been
created):
.CS
xeon07$ mkdir -p \(ti/.config/nix
.CE
And add the following line in the
.CI \(ti/.config/nix/nix.conf
file, replacing it with the path you copied before:
.CS
.SM
extra-sandbox-paths = /garlic=/home/Computational/rarias/.cache/garlic
.CE
This option creates a virtual directory called
.CI /garlic
inside the build environment, whose contents are the ones you specify at
the right hand side of the equal sign (in this case the
.CI \(ti/.cache/garlic
directory). It will be used to allow the results of the experiments to
be passed to nix from the
.I target
machine.
.\" ###################################################################
.NH 3
Run the garlic daemon (optional)
.LP
The garlic benchmark has a daemon which can be used to
automatically launch the experiments in the
.I target
machine on demand, when they are required to build other derivations, so
they can be launched without user interaction. The daemon creates some
FIFO pipes to communicate with the build environment, and must be
running to be able to run the experiments. To execute it, go to the
.CI bscpkgs/garlic
directory and run
.CS
xeon07$ nix-shell
nix-shell$
.CE
to enter the nix shell (or specify the path to the
.CI garlic/shell.nix
file as argument). Then, run the daemon inside the nix shell:
.CS
nix-shell$ garlicd
garlicd: Waiting for experiments ...
.CE
Notice that the daemon stays running in the foreground, waiting for
experiments. At this moment, it can only process one experiment at a
time.
.\" ===================================================================
.NH 2
Configure the login and target (MareNostrum 4)
.LP
In order to execute the programs in MareNostrum 4, you first need load
some utilities in the PATH. Add to the end of the file
\f(CW\(ti/.bashrc\fP in MareNostrum 4 the following line:
.CS
export PATH=/gpfs/projects/bsc15/nix/bin:$PATH
.CE
Then logout and login again (our source the \f(CW\(ti/.bashrc\fP file)
and check that now you have the \f(CWnix-develop\fP command available:
.CS
login1$ which nix-develop
/gpfs/projects/bsc15/nix/bin/nix-develop
.CE
The new utilities are available both in the login nodes and in the
compute (target) nodes, as they share the file system over the network.
.\" ===================================================================
.bp
.NH 1
Development
.LP
During the development phase, a functional program is produced by
modifying its source code. This process is generally cyclic: the
developer needs to compile, debug and correct mistakes. We want to
minimize the delay times, so the programs can be executed as soon as
needed, but under a controlled environment so that the same behavior
occurs during the experimentation phase.
.PP
In particular, we want that several developers can reproduce the
same development environment so they can debug each other programs
when reporting bugs. Therefore, the environment must be carefully
controlled to avoid non-reproducible scenarios.
.PP
The current development environment provides an isolated shell with a
clean environment, which runs in a new mount namespace where access to
the filesystem is restricted. Only the project directory and the nix
store are available (with some other exceptions), to ensure that you
cannot accidentally link with the wrong library or modify the build
process with a forgotten environment variable in the \f(CW\(ti/.bashrc\fP
file.
.\" ===================================================================
.NH 2
Getting the development tools
.LP
To create a development
environment, first copy or download the sources of your program (not the
dependencies) in a new directory placed in the target machine
(MareNostrum\~4).
.PP
The default environment contains packages commonly used to develop
programs, listed in the \fIgarlic/index.nix\fP file:
.\" FIXME: Unify garlic.unsafeDevelop in garlic.develop, so we can
.\" specify the packages directly
.CS
develop = let
commonPackages = with self; [
coreutils htop procps-ng vim which strace
tmux gdb kakoune universal-ctags bashInteractive
glibcLocales ncurses git screen curl
# Add more nixpkgs packages here...
];
bscPackages = with bsc; [
slurm clangOmpss2 icc mcxx perf tampi impi
# Add more bsc packages here...
];
...
.CE
If you need additional packages, add them to the list, so that they
become available in the environment. Those may include any dependency
required to build your program.
.PP
Then use the build machine (xeon07) to build the
.I garlic.develop
derivation:
.CS
build% nix-build -A garlic.develop
\&...
build% grep ln result
ln -fs /gpfs/projects/.../bin/stage1 .nix-develop
.CE
Copy the \fIln\fP command and run it in the target machine
(MareNostrum\~4), inside the new directory used for your program
development, to create the link \fI.nix-develop\fP (which is used to
remember your environment). Several environments can be stored in
different directories using this method, with different packages in each
environment. You will need
to rebuild the
.I garlic.develop
derivation and update the
.I .nix-develop
link after the package list is changed. Once the
environment link is created, there is no need to repeat these steps again.
.PP
Before entering the environment, you will need to access the required
resources for your program, which may include several compute nodes.
.\" ===================================================================
.NH 2
Allocating resources for development
.LP
Our target machine (MareNostrum 4) provides an interactive shell, that
can be requested with the number of computational resources required for
development. To do so, connect to the login node and allocate an
interactive session:
.CS
% ssh mn1
login% salloc ...
target%
.CE
This operation may take some minutes to complete depending on the load
of the cluster. But once the session is ready, any subsequent execution
of programs will be immediate.
.\" ===================================================================
.NH 2
Accessing the developement environment
.PP
The utility program \fInix-develop\fP has been designed to access the
development environment of the current directory, by looking for the
\fI.nix-develop\fP file. It creates a namespace where the required
packages are installed and ready to be used. Now you can access the
newly created environment by running:
.CS
target% nix-develop
develop%
.CE
The spawned shell contains all the packages pre-defined in the
\fIgarlic.develop\fP derivation, and can now be accessed by typing the
name of the commands.
.CS
develop% which gcc
/nix/store/azayfhqyg9...s8aqfmy-gcc-wrapper-9.3.0/bin/gcc
develop% which gdb
/nix/store/1c833b2y8j...pnjn2nv9d46zv44dk-gdb-9.2/bin/gdb
.CE
If you need additional packages, you can add them in the
\fIgarlic/index.nix\fP file as mentioned previously. To keep the
same current resources, so you don't need to wait again for the
resources to be allocated, exit only from the development shell:
.CS
develop% exit
target%
.CE
Then update the
.I .nix-develop
link and enter into the new develop environment:
.CS
target% nix-develop
develop%
.CE
.\" ===================================================================
.NH 2
Execution
.LP
The allocated shell can only execute tasks in the current node, which
may be enough for some tests. To do so, you can directly run your
program as:
.CS
develop$ ./program
.CE
If you need to run a multi-node program, typically using MPI
communications, then you can do so by using srun. Notice that you need
to allocate several nodes when calling salloc previously. The srun
command will execute the given program \fBoutside\fP the development
environment if executed as-is. So we re-enter the develop environment by
calling nix-develop as a wrapper of the program:
.\" FIXME: wrap srun to reenter the develop environment by its own
.CS
develop$ srun nix-develop ./program
.CE
.\" ===================================================================
.NH 2
Debugging
.LP
The debugger can be used to directly execute the program if is executed
in only one node by using:
.CS
develop$ gdb ./program
.CE
Or it can be attached to an already running program by using its PID.
You will need to first connect to the node running it (say target2), and
run gdb inside the nix-develop environment. Use
.I squeue
to see the compute nodes running your program:
.CS
login$ ssh target2
target2$ cd project-develop
target2$ nix-develop
develop$ gdb -p $pid
.CE
You can repeat this step to control the execution of programs running in
different nodes simultaneously.
.PP
In those cases where the program crashes before being able to attach the
debugger, enable the generation of core dumps:
.CS
develop$ ulimit -c unlimited
.CE
And rerun the program, which will generate a core file that can be
opened by gdb and contains the state of the memory when the crash
happened. Beware that the core dump file can be very large, depending on
the memory used by your program at the crash.
.\" ===================================================================
.NH 2
Git branch name convention
.LP
The garlic benchmark imposes a set of requirements to be meet for each
application in order to coordinate the execution of the benchmark and
the gathering process of the results.
.PP
Each application must be available in a git repository so it can be
included into the garlic benchmark. The different combinations of
programming models and communication schemes should be each placed in
one git branch, which are referred to as \fIbenchmark branches\fP. At
least one benchmark branch should exist and they all must begin with the
prefix \f(CWgarlic/\fP (other branches will be ignored).
.PP
The branch name is formed by adding keywords separated by the "+"
character. The keywords must follow the given order and can only
appear zero or once each. At least one keyword must be included. The
following keywords are available:
.IP \f(CWmpi\fP 5m
A significant fraction of the communications uses only the standard MPI
(without extensions like TAMPI).
.IP \f(CWtampi\fP
A significant fraction of the communications uses TAMPI.
.IP \f(CWsend\fP
A significant part of the MPI communication uses the blocking family of
methods
.I MPI_Send , (
.I MPI_Recv ,
.I MPI_Gather "...)."
.IP \f(CWisend\fP
A significant part of the MPI communication uses the non-blocking family
of methods
.I MPI_Isend , (
.I MPI_Irecv ,
.I MPI_Igather "...)."
.IP \f(CWrma\fP
A significant part of the MPI communication uses remote memory access
(one-sided) methods
.I MPI_Get , (
.I MPI_Put "...)."
.IP \f(CWseq\fP
The complete execution is sequential in each process (one thread per
process).
.IP \f(CWomp\fP
A significant fraction of the execution uses the OpenMP programming
model.
.IP \f(CWoss\fP
A significant fraction of the execution uses the OmpSs-2 programming
model.
.IP \f(CWtask\fP
A significant part of the execution involves the use of the tasking
model.
.IP \f(CWtaskfor\fP
A significant part of the execution uses the taskfor construct.
.IP \f(CWfork\fP
A significant part of the execution uses the fork-join model (including
hybrid programming techniques with parallel computations and sequential
communications).
.IP \f(CWsimd\fP
A significant part of the computation has been optimized to use SIMD
instructions.
.LP
In the
.URL #appendixA "Appendix A"
there is a flowchart to help the decision
process of the branch name. Additional user defined keywords may be
added at the end using the separator "+" as well. User keywords must
consist of capital alphanumeric characters only and be kept short. These
additional keywords must be different (case insensitive) to the already
defined above. Some examples:
.CS
garlic/mpi+send+seq
garlic/mpi+send+omp+fork
garlic/mpi+isend+oss+task
garlic/tampi+isend+oss+task
garlic/tampi+isend+oss+task+COLOR
garlic/tampi+isend+oss+task+COLOR+BTREE
.CE
.\" ===================================================================
.NH 2
Initialization time
.LP
It is common for programs to have an initialization phase prior to the
execution of the main computation task which is the objective of the study.
The initialization phase is usually not considered when taking
measurements, but the time it takes to complete can limit seriously the
amount of information that can be extracted from the computation phase.
As an example, if the computation phase is in the order of seconds, but
the initialization phase takes several minutes, the number of runs would
need to be set low, as the units could exceed the time limits. Also, the
experimenter may be reluctant to modify the experiments to test other
parameters, as the waiting time for the results is unavoidably large.
.PP
To prevent this problem the programs must reduce the time of the
initialization phase to be no larger than the computation time. To do
so, the initialization phase can be optimized either with
parallelization, or it can be modified to store the result of the
initialization to the disk to be later at the computation phase. In the
garlic framework an experiment can have a dependency over the results of
another experiment (the results of the initialization). The
initialization results will be cached if the derivation is kept
invariant, when modifying the computation phase parameters.
.\" ===================================================================
.NH 2
Measurement of the execution time
.LP
The programs must measure the wall time of the computation phase following a
set of rules. The way in which the wall time is measured is very important to
get accurate results. The measured time must be implemented by using a
monotonic clock which is able to correct the drift of the oscillator of
the internal clock due to changes in temperature. This clock must be
measured in C and C++ with:
.CS
clock_gettime(CLOCK_MONOTONIC, &ts);
.CE
A helper function can be used the approximate value of the clock in a
double precision float, in seconds:
.CS
double get_time()
{
struct timespec tv;
if(clock_gettime(CLOCK_MONOTONIC, &tv) != 0)
{
perror("clock_gettime failed");
exit(EXIT_FAILURE);
}
return (double)(ts.tv_sec) +
(double)ts.tv_nsec * 1.0e-9;
}
.CE
The start and end points must be measured after the synchronization of
all the processes and threads, so the complete computation work can be
bounded to fit inside the measured interval. An example for a MPI
program:
.CS
double start, end, delta_time;
MPI_Barrier();
start = get_time();
run_simulation();
MPI_Barrier();
end = get_time();
delta_time = end - start;
.CE
.\" ===================================================================
.NH 2
Format of the execution time
.LP
The measured execution time must be printed to the standard output
(stdout) in scientific notation with at least 7 significative digits.
The following the printf format (or the strict equivalent in other languages)
must be used:
.CS
printf("time %e\\n", delta_time);
.CE
The line must be printed alone and only once: for MPI programs,
only one process shall print the time:
.CS
if(rank == 0) printf("time %e\\n", delta_time);
.CE
Other lines can be printed in the stdout, but without the
.I time
prefix, so that the following pipe can be used to capture the line:
.CS
% ./app | grep "^time"
1.234567e-01
.CE
Ensure that your program follows this convention by testing it with the
above
.I grep
filter; otherwise the results will fail to be parsed when building
the dataset with the execution time.
.\" ===================================================================
.bp
.NH 1
Experimentation
.LP
During the experimentation, a program is studied by running it and
measuring some properties. The experimenter is in charge of the
experiment design, which is typically controlled by a single
.I nix
file placed in the
.CI garlic/exp
subdirectory.
Experiments are formed by several
.I "experimental units"
or simply
.I units .
A unit is the result of each unique configuration of the experiment
(typically involves the cartesian product of all factors) and
consists of several shell scripts executed sequentially to setup the
.I "execution environment" ,
which finally launch the actual program being analyzed.
The scripts that prepare the environment and the program itself are
called the
.I stages
of the execution and altogether form the
.I "execution pipeline"
or simply the
.I pipeline .
The experimenter must know with very good details all the stages
involved in the pipeline, as they have a large impact on the execution.
.PP
Additionally, the execution time is impacted by the target machine in
which the experiments run. The software used for the benchmark is
carefully configured and tuned for the hardware used in the execution;
in particular, the experiments are designed to run in MareNostrum 4
cluster with the SLURM workload manager and the Omni-Path
interconnection network. In the future we plan to add
support for other clusters in order to execute the experiments in other
machines.
.\"#####################################################################
.NH 2
Isolation
.LP
The benchmark is designed so that both the compilation of every software
package and the execution of the experiment is performed under strict
conditions. We can ensure that two executions of the same experiment are
actually running the same program in the same software environment.
.PP
All the software used by an experiment is included in the
.I "nix store"
which is, by convention, located at the
.CI /nix
directory. Unfortunately, it is common for libraries to try to load
software from other paths like
.CI /usr
or
.CI /lib .
It is also common that configuration files are loaded from
.CW /etc
and from the home directory of the user that runs the experiment.
Additionally, some environment variables are recognized by the libraries
used in the experiment, which change their behavior. As we cannot
control the software and configuration files in those directories, we
couldn't guarantee that the execution behaves as intended.
.PP
In order to avoid this problem, we create a
.I sandbox
where only the files in the nix store are available (with some other
exceptions). Therefore, even if the libraries try to access any path
outside the nix store, they will find that the files are not there
anymore. Additionally, the environment variables are cleared before
entering the environment (with some exceptions as well).
.\"#####################################################################
.NH 2
Execution pipeline
.LP
Several predefined stages form the
.I standard
execution pipeline and are defined in the
.I stdPipeline
array. The standard pipeline prepares the resources and the environment
to run a program (usually in parallel) in the compute nodes. It is
divided in two main parts:
connecting to the target machine to submit a job and executing the job.
Finally, the complete execution pipeline ends by running the actual
program, which is not part of the standard pipeline, as should be
defined differently for each program.
.\"#####################################################################
.NH 3
Job submission
.LP
Some stages are involved in the job submission: the
.I trebuchet
stage connects via
.I ssh
to the target machine and executes the next stage there. Once in the
target machine, the
.I runexp
stage computes the output path to store the experiment results, using
the user in the target machine and changes the working directory there.
In MareNostrum 4 the output path is at
.CI /gpfs/projects/bsc15/garlic/$user/out .
Then the
.I isolate
stage is executed to enter the sandbox and the
.I experiment
stage begins, which creates a directory to store the experiment output,
and launches several
.I unit
stages.
.PP
Each unit executes a
.I sbatch
stage which runs the
.I sbatch(1)
program with a job script that simply calls the next stage. The
sbatch program internally reads the
.CW /etc/slurm/slurm.conf
file from outside the sandbox, so we must explicitly allow this file to
be available, as well as the
.I munge
socket used for authentication by the SLURM daemon. Once the jobs are
submitted to SLURM, the experiment stage ends and the trebuchet finishes
the execution. The jobs will be queued for execution without any other
intervention from the user.
.PP
The rationale behind running sbatch from the sandbox is because the
options provided in environment variables override the options from the
job script. Therefore, we avoid this problem by running sbatch from the
sandbox, where the interfering environment variables are removed. The
sbatch program is also provided in the
.I "nix store" ,
with a version compatible with the SLURM daemon running in the target
machine.
.\"#####################################################################
.NH 3
Job execution
.LP
Once an unit job has been selected for execution, SLURM
allocates the resources (usually several nodes) and then selects one of
the nodes to run the job script: it is not executed in parallel yet.
The job script runs from a child process forked from on of the SLURM
daemon processes, which are outside the sandbox. Therefore, we first run the
.I isolate
stage
to enter the sandbox again.
.PP
The next stage is called
.I control
and determines if enough data has been generated by the experiment unit
or if it should continue repeating the execution. At the current time,
it is only implemented as a simple loop that runs the next stage a fixed
amount of times (by default, it is repeated 30 times).
.PP
The following stage is
.I srun
which launches several copies of the next stage to run in
parallel (when using more than one task). Runs one copy per task,
effectively creating one process per task. The CPUs affinity is
configured by the parameter
.I --cpu-bind
and is important to set it correctly (see more details in the
.I srun(1)
manual). Appending the
.I verbose
value to the cpu bind option causes srun to print the assigned affinity
of each task, which is very valuable when examining the execution log.
.PP
The mechanism by which srun executes multiple processes is the same used
by sbatch, it forks from a SLURM daemon running in the computing nodes.
Therefore, the execution begins outside the sandbox. The next stage is
.I isolate
which enters again the sandbox in every task. All remaining stages are
running now in parallel.
.\" ###################################################################
.NH 3
The program
.LP
At this point in the execution, the standard pipeline has been
completely executed, and we are ready to run the actual program that is
the matter of the experiment. Usually, programs require some arguments
to be passed in the command line. The
.I exec
stage sets the arguments (and optionally some environment variables) and
executes the last stage, the
.I program .
.PP
The experimenters are required to define these last stages, as they
define the specific way in which the program must be executed.
Additional stages may be included before or after the program run, so
they can perform additional steps.
.\" ###################################################################
.NH 3
Stage overview
.LP
The complete execution pipeline using the standard pipeline is shown in
the Table 1. Some properties are also reflected about the execution
stages.
.DS L
.TS
center;
lB cB cB cB cB cB
l c c c c c.
_
Stage Where Safe Copies User Std
_
trebuchet * no no yes yes
runexp login no no no yes
isolate login no no no yes
experiment login yes no no yes
unit login yes no no yes
sbatch login yes no no yes
_
isolate target no no no yes
control target yes no no yes
srun target yes no no yes
isolate target no yes no yes
_
exec target yes yes no no
program target yes yes no no
_
.TE
.DE
.QS
.SM
.B "Table 1" :
The stages of a complete execution pipeline. The
.I where
column determines where the stage is running,
.I safe
states if the stage begins the execution inside the sandbox,
.I user
if it can be executed directly by the user,
.I copies
if there are several instances running in parallel and
.I std
if is part of the standard execution pipeline.
.QE
.\" ###################################################################
.NH 2
Writing the experiment
.LP
The experiments are generally written in the
.I nix
language as it provides very easy management for the packages an their
customization. An experiment file is formed by several parts, which
produce the execution pipeline when built. The experiment file describes
a function (which is typical in nix) and takes as argument an
attribute set with some common packages, tools and options:
.CS
{ stdenv, lib, bsc, stdexp, targetMachine, stages, garlicTools }:
.CE
The
.I bsc
attribute contains all the BSC and nixpkgs packages, as defined in the
overlay. The
.I stdexp
contains some useful tools and functions to build the experiments, like
the standard execution pipeline, so you don't need to redefine the
stages in every experiment. The configuration of the target machine is
specified in the
.I targetMachine
attribute which includes information like the number of CPUs per node or
the cache line length. It is used to define the experiments in such a
way that they are not tailored to an specific machine hardware
(sometimes this is not posible). All the execution stages are available
in the
.I stages
attribute which are used when some extra stage is required. And finally,
the
.I garlicTools
attribute provide some functions to aid common tasks when defining the
experiment configuration
.\" ###################################################################
.NH 3
Experiment configuration
.LP
The next step is to define some variables in a
.CI let
\&...
.CI in
\&...
.CI ;
construct, to be used later. The first one, is the variable
configuration of the experiment called
.I varConf ,
which include all
the factors that will be changed. All the attributes of this set
.I must
be arrays, even if they only contain one element:
.CS
varConf = {
blocks = [ 1 2 4 ];
nodes = [ 1 ];
};
.CE
In this example, the variable
.I blocks
will be set to the values 1, 2 and 4; while
.I nodes
will remain set to 1 always. These variables are used later to build the
experiment configuration. The
.I varConf
is later converted to a list of attribute sets, where every attribute
contains only one value, covering all the combinations (the Cartesian
product is computed):
.CS
[ { blocks = 1; nodes = 1; }
{ blocks = 2; nodes = 1; }
{ blocks = 4; nodes = 1; } ]
.CE
These configurations are then passed to the
.I genConf
function one at a time, which is the central part of the description of
the experiment:
.CS
genConf = var: fix (self: targetMachine.config // {
expName = "example";
unitName = self.expName + "-b" + toString self.blocks;
blocks = var.blocks;
cpusPerTask = 1;
tasksPerNode = self.hw.socketsPerNode;
nodes = var.nodes;
});
.CE
It takes as input
.I one
configuration from the Cartesian product, for example:
.CS
{ blocks = 2; nodes = 1; }
.CE
And returns the complete configuration for that input, which usually
expand the input configuration with some derived variables along with
other constant parameters. The return value can be inspected by calling
the function in the interactive
.I "nix repl"
session:
.CS
nix-repl> genConf { blocks = 2; nodes = 1; }
{
blocks = 2;
cpusPerTask = 1;
expName = "example";
hw = { ... };
march = "skylake-avx512";
mtune = "skylake-avx512";
name = "mn4";
nixPrefix = "/gpfs/projects/bsc15/nix";
nodes = 1;
sshHost = "mn1";
tasksPerNode = 2;
unitName = "example-b2";
}
.CE
Some configuration parameters were added by
.I targetMachine.config ,
such as the
.I nixPrefix ,
.I sshHost
or the
.I hw
attribute set, which are specific for the cluster they experiment is
going to run. Also, the
.I unitName
got assigned the proper name based on the number of blocks, but the
number of tasks per node were assigned based on the hardware description
of the target machine.
.PP
By following this rule, the experiments can easily be ported to machines
with other hardware characteristics, and we only need to define the
hardware details once. Then all the experiments will be updated based on
those details.
.\" ###################################################################
.NH 3
Adding the stages
.LP
Once the configuration is ready, it will be passed to each stage of the
execution pipeline which will take the parameters it needs. The
connection between the parameters and how they are passed to each stage
is done either by convention or manually. There is a list of parameters that
are recognized by the standard pipeline stages. For example the
attribute
.I nodes ,
it is recognized as the number of nodes in the standard
.I sbatch
stage when allocating resources:
.DS L
.TS
center;
lB lB cB cB lB
l l c c l.
_
Stage Attribute Std Req Description
_
* nixPrefix yes yes Path to the nix store in the target
unit expName yes yes Name of the experiment
unit unitName yes yes Name of the unit
control loops yes yes Number of runs of each unit
sbatch cpusPerTask yes yes Number of CPUs per task (process)
sbatch jobName yes yes Name of the job
sbatch nodes yes yes Number of nodes allocated
sbatch ntasksPerNode yes yes Number of tasks (processes) per node
sbatch qos yes no Name of the QoS queue
sbatch reservation yes no Name of the reservation
sbatch time yes no Maximum allocated time (string)
_
exec argv no no Array of arguments to execve
exec env no no Environment variable settings
exec pre no no Code before the execution
exec post no no Code after the execution
_
.TE
.DE
.QS
.SM
.B "Table 2" :
The attributes recognized by the stages in the execution pipeline. The
column
.I std
indicates if they are part of the standard execution pipeline. Some
attributes are required as indicated by the
.I req
column.
.QE
.LP
Other attribute names can be used to specify custom information used in
additional stages. The two most common stages required to complete the
pipeline are the
.I exec
and the
.I program .
Let see an example of
.I exec :
.CS
exec = {nextStage, conf, ...}: stages.exec {
inherit nextStage;
argv = [ "--blocks" conf.blocks ];
};
.CE
The
.I exec
stage is defined as a function that uses the predefined
.I stages.exec
stage, which accepts the
.I argv
array, and sets the argv of the program. In our case, we fill the
.I argv
array by setting the
.I --blocks
parameter to the number of blocks, specified in the configuration in the
attribute
.I blocks .
The name of this attribute can be freely choosen, as long as the
.I exec
stage refers to it properly. The
.I nextStage
attribute is mandatory in all stages, and is automatically set when
building the pipeline.
.PP
The last step is to configure the actual program to be executed,
which can be specified as another stage:
.CS
program = {nextStage, conf, ...}: bsc.apps.example;
.CE
Notice that this function only returns the
.I bsc.apps.example
derivation, which will be translated to the path where the example
program is installed. If the program is located inside a directory
(typically
.I bin ),
it must define the attribute
.I programPath
in the
.I bsc.apps.example
derivation, which points to the executable program. An example:
.CS
stdenv.mkDerivation {
\& ...
programPath = "/bin/example";
\& ...
};
.CE
.\" ###################################################################
.NH 3
Building the pipeline
.LP
With the
.I exec
and
.I program
stages defined and the ones provided by the standard pipeline, the
complete execution pipeline can be formed. To do so, the stages are
placed in an array, in the order they will be executed:
.CS
pipeline = stdexp.stdPipeline ++ [ exec program ];
.CE
The attribute
.I stdexp.stdPipeline
contains the standard pipeline stages, and we only append our two
defined stages
.I exec
and
.I program .
The
.I pipeline
is an array of functions, and must be transformed in something that can
be executed in the target machine. For that purpose, the
.I stdexp
provides the
.I genExperiment
function, which takes the
.I pipeline
array and the list of configurations and builds the execution pipeline:
.CS
stdexp.genExperiment { inherit configs pipeline; }
.CE
The complete example experiment can be shown here:
.CS
{ stdenv, lib, stdexp, bsc, targetMachine, stages }:
with lib;
let
# Initial variable configuration
varConf = {
blocks = [ 1 2 4 ];
nodes = [ 1 ];
};
# Generate the complete configuration for each unit
genConf = c: targetMachine.config // rec {
expName = "example";
unitName = "${expName}-b${toString blocks}";
inherit (targetMachine.config) hw;
inherit (c) blocks nodes;
loops = 30;
ntasksPerNode = hw.socketPerNode;
cpusPerTask = hw.cpusPerSocket;
jobName = unitName;
};
# Compute the array of configurations
configs = stdexp.buildConfigs {
inherit varConf genConf;
};
exec = {nextStage, conf, ...}: stages.exec {
inherit nextStage;
argv = [ "--blocks" conf.blocks ];
};
program = {nextStage, conf, ...}: bsc.garlic.apps.example;
pipeline = stdexp.stdPipeline ++ [ exec program ];
in
stdexp.genExperiment { inherit configs pipeline; }
.CE
.\" ###################################################################
.NH 3
Adding the experiment to the index
.LP
The experiment file must be located in a named directory inside the
.I garlic/exp
directory. The name is usually the program name. Once the experiment is
placed in a nix file, it must be added to the index of experiments, so
it can be build. The index is hyerarchically organized as attribute
sets, with
.I exp
containing all the experiments;
.I exp.example
the experiments of the
.I example
program; and
.I exp.example.test1
referring to the
.I test1
experiment of the
.I example
program. Additional attributes can be added, like
.I exp.example.test1.variantA
to handle more details.
.PP
For this example we are going to use the attribute path
.I exp.example.test
and add it to the index, in the
.I garlic/exp/index.nix
file. We append to the end of the attribute set, the following
definition:
.CS
\&...
example = {
test = callPackage ./example/test.nix { };
};
}
.CE
The experiment can now be built with:
.CS
builder% nix-build -A exp.example.test
.CE
.\" ###################################################################
.NH 2
Recommendations
.PP
The complete results generally take a long time to be finished, so it is
advisable to design the experiments iteratively, in order to quickly
obtain some feedback. Some recommendations:
.BL
.LI
Start with one unit only.
.LI
Set the number of runs low (say 5) but more than one.
.LI
Use a small problem size, so the execution time is low.
.LI
Set the time limit low, so deadlocks are caught early.
.LE
.PP
As soon as the first runs are complete, examine the results and test
that everything looks good. You would likely want to check:
.BL
.LI
The resources where assigned as intended (nodes and CPU affinity).
.LI
No errors or warnings: look at stderr and stdout logs.
.LI
If a deadlock happens, it will run out of the time limit.
.LE
.PP
As you gain confidence over that the execution went as planned, begin
increasing the problem size, the number of runs, the time limit and
lastly the number of units. The rationale is that each unit that is
shared among experiments gets assigned the same hash. Therefore, you can
iteratively add more units to an experiment, and if they are already
executed (and the results were generated) is reused.
.\" ###################################################################
.bp
.NH 1
Post-processing
.LP
After the correct execution of an experiment the results are stored for
further investigation. Typically the time of the execution or other
quantities are measured and presented later in a figure (generally a
plot or a table). The
.I "postprocess pipeline"
consists of all the steps required to create a set of figures from the
results. Similarly to the execution pipeline where several stages run
sequentially,
.[
garlic execution
.]
the postprocess pipeline is also formed by multiple stages executed
in order.
.PP
The rationale behind dividing execution and postprocess is
that usually the experiments are costly to run (they take a long time to
complete) while generating a figure require less time. Refining the
figures multiple times reusing the same experimental results doesn't
require the execution of the complete experiment, so the experimenter
can try multiple ways to present the data without waiting a large delay.
.NH 2
Results
.LP
The results are generated in the same
.I "target"
machine where the experiment is executed and are stored in the garlic
\fCout\fP
directory, organized into a tree structure following the experiment
name, the unit name and the run number (governed by the
.I control
stage):
.DS L
\fC
|-- 6lp88vlj7m8hvvhpfz25p5mvvg7ycflb-experiment
| |-- 8lpmmfix52a8v7kfzkzih655awchl9f1-unit
| | |-- 1
| | | |-- stderr.log
| | | |-- stdout.log
| | | |-- ...
| | |-- 2
\&...
\fP
.DE
In order to provide an easier access to the results, an index is also
created by taking the
.I expName
and
.I unitName
attributes (defined in the experiment configuration) and linking them to
the appropriate experiment and unit directories. These links are
overwritten by the last experiment with the same names so they are only
valid for the last execution. The out and index directories are
placed into a per-user directory, as we cannot guarantee the complete
execution of each unit when multiple users share units.
.PP
The messages printed to
.I stdout
and
.I stderr
are stored in the log files with the same name inside each run
directory. Additional data is sometimes generated by the experiments,
and is found in each run directory. As the generated data can be very
large, is ignored by default when fetching the results.
.NH 2
Fetching the results
.LP
Consider a program of interest for which an experiment has been designed to
measure some properties that the experimenter wants to present in a
visual plot. When the experiment is launched, the execution
pipeline (EP) is completely executed and it will generate some
results. In this escenario, the execution pipeline depends on the
program\[em]any changes in the program will cause nix to build the
pipeline again
using the updated program. The results will also depend on the
execution pipeline as well as the postprocess pipeline (PP) and the plot
on the results. This chain of dependencies can be shown in the
following dependency graph:
.PS
circlerad=0.22;
linewid=0.3;
right
circle "Prog"
arrow
circle "EP"
arrow
circle "Result"
arrow
circle "PP"
arrow
circle "Plot"
.PE
Ideally, the dependencies should be handled by nix, so it can detect any
change and rebuild the necessary parts automatically. Unfortunately, nix
is not able to build the result as a derivation directly, as it requires
access to the
.I "target"
machine with several user accounts. In order to let several users reuse
the same results from a shared cache, we would like to use the
.I "nix store" .
.PP
To generate the results from the
experiment, we add some extra steps that must be executed manually:
.PS
circle "Prog"
arrow
diag=linewid + circlerad;
far=circlerad*3 + linewid*4
E: circle "EP"
R: circle "Result" at E + (far,0)
RUN: circle "Run" at E + (diag,-diag) dashed
FETCH: circle "Fetch" at R + (-diag,-diag) dashed
move to R.e
arrow
P: circle "PP"
arrow
circle "Plot"
arrow dashed from E to RUN chop
arrow dashed from RUN to FETCH chop
arrow dashed from FETCH to R chop
arrow from E to R chop
.PE
The run and fetch steps are provided by the helper tool
.I "garlic(1)" ,
which launches the experiment using the user credentials at the
.I "target"
machine and then fetches the results, placing them in a directory known
by nix. When the result derivation needs to be built, nix will look in
this directory for the results of the execution. If the directory is not
found, a message is printed to suggest the user to launch the experiment
and the build process is stopped. When the result is successfully built
by any user, is stored in the
.I "nix store"
and it won't need to be rebuilt again until the experiment changes, as
the hash only depends on the experiment and not on the contents of the
results.
.PP
Notice that this mechanism violates the deterministic nature of the nix
store, as from a given input (the experiment) we can generate different
outputs (each result from different executions). We knowingly relaxed
this restriction by providing a guarantee that the results are
equivalent and there is no need to execute an experiment more than once.
.PP
To force the execution of an experiment you can use the
.I rev
attribute which is a number assigned to each experiment
and can be incremented to create copies that only differs on that
number. The experiment hash will change but the experiment will be the
same, as long as the revision number is ignored along the execution
stages.
.NH 2
Postprocess stages
.LP
Once the results are completely generated in the
.I "target"
machine there are several stages required to build a set of figures:
.PP
.I fetch \[em]
waits until all the experiment units are completed and then executes the
next stage. This stage is performed by the
.I garlic(1)
tool using the
.I -F
option and also reports the current state of the execution.
.PP
.I store \[em]
copies from the
.I target
machine into the nix store all log files generated by the experiment,
keeping the same directory structure. It tracks the execution state of
each unit and only copies the results once the experiment is complete.
Other files are ignored as they are often very large and not required
for the subsequent stages.
.PP
.I timetable \[em]
converts the results of the experiment into a NDJSON file with one
line per run for each unit. Each line is a valid JSON object, containing
the
.I exp ,
.I unit
and
.I run
keys and the unit configuration (as a JSON object) in the
.I config
key. The execution time is captured from the standard output and is
added in the
.I time
key.
.PP
.I merge \[em]
one or more timetable datasets are joined, by simply concatenating them.
This step allows building one dataset to compare multiple experiments in
the same figure.
.PP
.I rPlot \[em]
one ot more figures are generated by a single R script
.[
r cookbook
.]
which takes as input the previously generated dataset.
The path of the dataset is recorded in the figure as well, which
contains enough information to determine all the stages in the execution
and postprocess pipelines.
.NH 2
Current setup
.LP
As of this moment, the
.I build
machine which contains the nix store is
.I xeon07
and the
.I "target"
machine used to run the experiments is Mare Nostrum 4 with the
.I output
directory placed at
.CW /gpfs/projects/bsc15/garlic .
By default, the experiment results are never deleted from the
.I target
so you may want to remove the ones already stored in the nix store to
free space.
.\" ###################################################################
.bp
.SH 1
Appendix A: Branch name diagram
.LP
.TAG appendixA
.DS B
.SM
.PS 4.4/25.4
copy "gitbranch.pic"
.PE
.DE