From c53c8c9ff521a9c9c3fa1a1ab4dde11502a393ba Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Tue, 28 Aug 2018 17:10:53 +0200 Subject: [PATCH 1/3] Adapt tutorial examples to build systems syntax --- tutorial/advanced/advanced_example1.py | 6 ++-- tutorial/advanced/advanced_example4.py | 5 ++-- tutorial/advanced/advanced_example8.py | 15 +++++----- tutorial/example2.py | 38 +++++++++++++------------- tutorial/example3.py | 16 +++++------ tutorial/example4.py | 12 ++++---- tutorial/example7.py | 6 ++-- tutorial/example8.py | 27 +++++++++--------- 8 files changed, 61 insertions(+), 64 deletions(-) diff --git a/tutorial/advanced/advanced_example1.py b/tutorial/advanced/advanced_example1.py index 55863c44fa..855be9ddd2 100644 --- a/tutorial/advanced/advanced_example1.py +++ b/tutorial/advanced/advanced_example1.py @@ -11,10 +11,8 @@ def __init__(self): self.valid_systems = ['*'] self.valid_prog_environs = ['*'] self.executable = './advanced_example1' + self.build_system = 'Make' + self.build_system.cppflags = ['-DMESSAGE'] self.sanity_patterns = sn.assert_found('SUCCESS', self.stdout) self.maintainers = ['put-your-name-here'] self.tags = {'tutorial'} - - def compile(self): - self.current_environ.cppflags = '-DMESSAGE' - super().compile() diff --git a/tutorial/advanced/advanced_example4.py b/tutorial/advanced/advanced_example4.py index 00fcbae3b2..685515bec7 100644 --- a/tutorial/advanced/advanced_example4.py +++ b/tutorial/advanced/advanced_example4.py @@ -13,9 +13,8 @@ def __init__(self): self.modules = ['cudatoolkit'] self.variables = {'CUDA_HOME': '$CUDATOOLKIT_HOME'} self.executable = './advanced_example4' + self.build_system = 'Make' + self.build_system.makefile = 'Makefile_example4' self.sanity_patterns = sn.assert_found(r'SUCCESS', self.stdout) self.maintainers = ['put-your-name-here'] self.tags = {'tutorial'} - - def compile(self): - super().compile(makefile='Makefile_example4') diff --git a/tutorial/advanced/advanced_example8.py b/tutorial/advanced/advanced_example8.py index 487843c8dc..cdce4fa6a9 100644 --- a/tutorial/advanced/advanced_example8.py +++ b/tutorial/advanced/advanced_example8.py @@ -10,11 +10,12 @@ def __init__(self, variant): self.valid_systems = ['daint:gpu', 'daint:mc'] self.valid_prog_environs = ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi'] + self.build_system = 'SingleSource' self.prgenv_flags = { - 'PrgEnv-cray': '-homp', - 'PrgEnv-gnu': '-fopenmp', - 'PrgEnv-intel': '-openmp', - 'PrgEnv-pgi': '-mp' + 'PrgEnv-cray': ['-homp'], + 'PrgEnv-gnu': ['-fopenmp'], + 'PrgEnv-intel': ['-openmp'], + 'PrgEnv-pgi': ['-mp'] } if variant == 'MPI': @@ -45,8 +46,8 @@ def __init__(self, variant): self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): + def setup(self, partition, environ, **job_opts): if self.prgenv_flags is not None: - self.current_environ.cflags = self.prgenv_flags[self.current_environ.name] + self.build_system.cflags = self.prgenv_flags[environ.name] - super().compile() + super().setup(partition, environ, **job_opts) diff --git a/tutorial/example2.py b/tutorial/example2.py index 3bf018faf6..0bb9df5939 100644 --- a/tutorial/example2.py +++ b/tutorial/example2.py @@ -11,6 +11,7 @@ def __init__(self): self.valid_prog_environs = ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_openmp.c' + self.build_system = 'SingleSource' self.executable_opts = ['1024', '100'] self.variables = { 'OMP_NUM_THREADS': '4' @@ -20,18 +21,17 @@ def __init__(self): self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): - env_name = self.current_environ.name - if env_name == 'PrgEnv-cray': - self.current_environ.cflags = '-homp' - elif env_name == 'PrgEnv-gnu': - self.current_environ.cflags = '-fopenmp' - elif env_name == 'PrgEnv-intel': - self.current_environ.cflags = '-openmp' - elif env_name == 'PrgEnv-pgi': - self.current_environ.cflags = '-mp' + def setup(self, partition, environ, **job_opts): + if environ.name == 'PrgEnv-cray': + self.build_system.cflags = ['-homp'] + elif environ.name == 'PrgEnv-gnu': + self.build_system.cflags = ['-fopenmp'] + elif environ.name == 'PrgEnv-intel': + self.build_system.cflags = ['-openmp'] + elif environ.name == 'PrgEnv-pgi': + self.build_system.cflags = ['-mp'] - super().compile() + super().setup(partition, environ, **job_opts) @rfm.simple_test @@ -43,12 +43,13 @@ def __init__(self, **kwargs): self.valid_prog_environs = ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_openmp.c' + self.build_system = 'SingleSource' self.executable_opts = ['1024', '100'] self.prgenv_flags = { - 'PrgEnv-cray': '-homp', - 'PrgEnv-gnu': '-fopenmp', - 'PrgEnv-intel': '-openmp', - 'PrgEnv-pgi': '-mp' + 'PrgEnv-cray': ['-homp'], + 'PrgEnv-gnu': ['-fopenmp'], + 'PrgEnv-intel': ['-openmp'], + 'PrgEnv-pgi': ['-mp'] } self.variables = { 'OMP_NUM_THREADS': '4' @@ -58,7 +59,6 @@ def __init__(self, **kwargs): self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): - prgenv_flags = self.prgenv_flags[self.current_environ.name] - self.current_environ.cflags = prgenv_flags - super().compile() + def setup(self, partition, environ, **job_opts): + self.build_system.cflags = self.prgenv_flags[environ.name] + super().setup(partition, environ, **job_opts) diff --git a/tutorial/example3.py b/tutorial/example3.py index 2efad40cd8..374ad2a9b1 100644 --- a/tutorial/example3.py +++ b/tutorial/example3.py @@ -12,11 +12,12 @@ def __init__(self): 'PrgEnv-intel', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_mpi_openmp.c' self.executable_opts = ['1024', '10'] + self.build_system = 'SingleSource' self.prgenv_flags = { - 'PrgEnv-cray': '-homp', - 'PrgEnv-gnu': '-fopenmp', - 'PrgEnv-intel': '-openmp', - 'PrgEnv-pgi': '-mp' + 'PrgEnv-cray': ['-homp'], + 'PrgEnv-gnu': ['-fopenmp'], + 'PrgEnv-intel': ['-openmp'], + 'PrgEnv-pgi': ['-mp'] } self.sanity_patterns = sn.assert_found( r'time for single matrix vector multiplication', self.stdout) @@ -29,7 +30,6 @@ def __init__(self): self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): - prgenv_flags = self.prgenv_flags[self.current_environ.name] - self.current_environ.cflags = prgenv_flags - super().compile() + def setup(self, partition, environ, **job_opts): + self.build_system.cflags = self.prgenv_flags[environ.name] + super().setup(partition, environ, **job_opts) diff --git a/tutorial/example4.py b/tutorial/example4.py index 94f320c324..b6ee11c39b 100644 --- a/tutorial/example4.py +++ b/tutorial/example4.py @@ -10,19 +10,19 @@ def __init__(self): self.valid_systems = ['daint:gpu'] self.valid_prog_environs = ['PrgEnv-cray', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_openacc.c' + self.build_system = 'SingleSource' self.executable_opts = ['1024', '100'] self.modules = ['craype-accel-nvidia60'] self.num_gpus_per_node = 1 self.prgenv_flags = { - 'PrgEnv-cray': '-hacc -hnoomp', - 'PrgEnv-pgi': '-acc -ta=tesla:cc60' + 'PrgEnv-cray': ['-hacc', '-hnoomp'], + 'PrgEnv-pgi': ['-acc', '-ta=tesla:cc60'] } self.sanity_patterns = sn.assert_found( r'time for single matrix vector multiplication', self.stdout) self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): - prgenv_flags = self.prgenv_flags[self.current_environ.name] - self.current_environ.cflags = prgenv_flags - super().compile() + def setup(self, partition, environ, **job_opts): + self.build_system.cflags = self.prgenv_flags[environ.name] + super().setup(partition, environ, **job_opts) diff --git a/tutorial/example7.py b/tutorial/example7.py index d279046655..a5adeef37a 100644 --- a/tutorial/example7.py +++ b/tutorial/example7.py @@ -10,6 +10,8 @@ def __init__(self): self.valid_systems = ['daint:gpu'] self.valid_prog_environs = ['PrgEnv-gnu', 'PrgEnv-cray', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_cuda.cu' + self.build_system = 'SingleSource' + self.build_system.cxxflags = ['-O3'] self.executable_opts = ['4096', '1000'] self.modules = ['cudatoolkit'] self.num_gpus_per_node = 1 @@ -26,7 +28,3 @@ def __init__(self): } self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - - def compile(self): - self.current_environ.cxxflags = '-O3' - super().compile() diff --git a/tutorial/example8.py b/tutorial/example8.py index 939c4a3acc..6c2c22ec5b 100644 --- a/tutorial/example8.py +++ b/tutorial/example8.py @@ -8,6 +8,7 @@ def __init__(self, test_version): self.descr = '%s matrix-vector multiplication' % test_version self.valid_systems = ['*'] self.valid_prog_environs = ['*'] + self.build_system = 'SingleSource' self.prgenv_flags = None matrix_dim = 1024 @@ -26,11 +27,11 @@ def __init__(self, test_version): self.maintainers = ['you-can-type-your-email-here'] self.tags = {'tutorial'} - def compile(self): + def setup(self, partition, environ, **job_opts): if self.prgenv_flags is not None: - self.current_environ.cflags = self.prgenv_flags[self.current_environ.name] + self.build_system.cflags = self.prgenv_flags[environ.name] - super().compile() + super().setup(partition, environ, **job_opts) @rfm.simple_test @@ -48,10 +49,10 @@ def __init__(self): self.valid_prog_environs = ['PrgEnv-cray', 'PrgEnv-gnu', 'PrgEnv-intel', 'PrgEnv-pgi'] self.prgenv_flags = { - 'PrgEnv-cray': '-homp', - 'PrgEnv-gnu': '-fopenmp', - 'PrgEnv-intel': '-openmp', - 'PrgEnv-pgi': '-mp' + 'PrgEnv-cray': ['-homp'], + 'PrgEnv-gnu': ['-fopenmp'], + 'PrgEnv-intel': ['-openmp'], + 'PrgEnv-pgi': ['-mp'] } self.variables = { 'OMP_NUM_THREADS': '4' @@ -67,10 +68,10 @@ def __init__(self): 'PrgEnv-intel', 'PrgEnv-pgi'] self.sourcepath = 'example_matrix_vector_multiplication_mpi_openmp.c' self.prgenv_flags = { - 'PrgEnv-cray': '-homp', - 'PrgEnv-gnu': '-fopenmp', - 'PrgEnv-intel': '-openmp', - 'PrgEnv-pgi': '-mp' + 'PrgEnv-cray': ['-homp'], + 'PrgEnv-gnu': ['-fopenmp'], + 'PrgEnv-intel': ['-openmp'], + 'PrgEnv-pgi': ['-mp'] } self.num_tasks = 8 self.num_tasks_per_node = 2 @@ -90,8 +91,8 @@ def __init__(self): self.modules = ['craype-accel-nvidia60'] self.num_gpus_per_node = 1 self.prgenv_flags = { - 'PrgEnv-cray': '-hacc -hnoomp', - 'PrgEnv-pgi': '-acc -ta=tesla:cc60' + 'PrgEnv-cray': ['-hacc', '-hnoomp'], + 'PrgEnv-pgi': ['-acc', '-ta=tesla:cc60'] } From 72247de3d3377d8318388273e706627a505acaf0 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 29 Aug 2018 15:54:00 +0200 Subject: [PATCH 2/3] Add documentation for the build systems feature - Tutorials were udpated - Reference guide was updated --- docs/advanced.rst | 76 ++++++++++--- docs/reference.rst | 29 +++++ docs/tutorial.rst | 178 ++++++++++++++++++++++++++---- reframe/core/buildsystems.py | 204 +++++++++++++++++++++++++++++++++-- reframe/core/pipeline.py | 14 +++ 5 files changed, 455 insertions(+), 46 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 1fd8ad38cf..f44df4ca8a 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -6,8 +6,8 @@ In this section, we are going to show some more elaborate use cases of ReFrame. Through the use of more advanced examples, we will demonstrate further customization options which modify the default options of the ReFrame pipeline. The corresponding scripts as well as the source code of the examples discussed here can be found in the directory ``tutorial/advanced``. -Leveraging Makefiles --------------------- +Working with Makefiles +---------------------- We have already shown how you can compile a single source file associated with your regression test. In this example, we show how ReFrame can leverage Makefiles to build executables. @@ -40,29 +40,50 @@ The contents of this regression test are the following (``tutorial/advanced/adva .. literalinclude:: ../tutorial/advanced/advanced_example1.py -The important bit here is the ``compile()`` method. +The important bit here is how we set up the build system for this test: .. literalinclude:: ../tutorial/advanced/advanced_example1.py - :lines: 18-20 + :lines: 14-15 :dedent: 4 -As in the simple single source file examples we showed in the `tutorial `__, we use the current programming environment's flags for modifying the compilation. -ReFrame will then compile the regression test source code as by invoking ``make`` as follows: -.. code-block:: bash +First, we set the build system to ``Make`` and then set the preprocessor flags for the compilation. +ReFrame will invoke ``make`` as follows: + +.. code:: + + make -j CC='cc' CXX='CC' FC='ftn' NVCC='nvcc' CPPFLAGS='-DMESSAGE' + +The compiler variables (``CC``, ``CXX`` etc.) are set based on the corresponding values specified in the `coniguration of the current environment `__. +You may instruct the build system to ignore the default values from the environment by setting the following: + +.. code-block:: python + + self.build_system.flags_from_environ = False + +In this case, ``make`` will be invoked as follows: + +.. code:: + + make -j CPPFLAGS='-DMESSAGE' + +Notice that the ``-j`` option is always generated. +If you want to limit build concurrency, you can do it as follows: + +.. code-block:: python - make CC=cc CXX=CC FC=ftn CPPFLAGS=-DMESSAGE + self.build_system.max_concurrency = 4 -Notice, how ReFrame passes all the programming environment's variables to the ``make`` invocation. -It is important to note here that, if a set of flags is set to :class:`None` (the default, if not otherwise set in the `ReFrame's configuration `__), these are not passed to ``make``. -You can also completely disable the propagation of any flags to ``make`` by setting :attr:`self.propagate = False ` in your regression test. -At this point it is useful also to note that you can also use a custom Makefile, not named ``Makefile`` or after any other standard Makefile name. -In this case, you can pass the custom Makefile name as an argument to the compile method of the base :class:`RegressionTest ` class as follows: +Finally, you may also customize the name of the ``Makefile``. +You can achieve that by setting the corresponding variable of the ``Make`` build system: .. code-block:: python - super().compile(makefile='Makefile_custom') + self.build_system.makefile = 'Makefile_custom' + + +More details on ReFrame's build systems, you may find `here `__. Retrieving the source code from a Git repository @@ -97,6 +118,29 @@ ReFrame will attempt to clone this repository inside the stage directory by exec You will have to specify it as URL using the ``ssh://`` protocol (see `Git documentation page `__). +Add a configuration step before compiling the code +================================================== + +It is often the case that a configuration step is needed before compiling a code with ``make``. +Currently, ReFrame does not provide specific abstractions for "configure-make"-style build systems. +However, you can achieve the same effect using the :attr:`prebuild_cmd ` for performing the configuration step. +The following code snippet will configure a code with ``cmake`` before invoking ``make``: + +.. code-block:: python + + self.prebuild_cmd = ['cmake -DUSE_LIBNUMA .'] + self.build_system = 'Make' + self.build_system.cppflags = ['-DHAVE_FOO'] + self.build_system.flags_from_environ = False + +The generated build script then will have the following lines: + +.. code-block:: bash + + cmake -DUSE_LIBNUMA . + make -j CPPFLAGS='-DHAVE_FOO' + + Implementing a Run-Only Regression Test --------------------------------------- @@ -159,10 +203,10 @@ At runtime, ReFrame will generate the following instructions in the shell script This ensures that the environment of the test is also set correctly at runtime. -Finally, as already mentioned `previously <#leveraging-makefiles>`__, since the ``Makefile`` name is not one of the standard ones, it has to be passed as an argument to the :func:`compile ` method of the base :class:`RegressionTest ` class as follows: +Finally, as already mentioned `previously <#working-with-makefiles>`__, since the name of the makefile is not one of the standard ones, it must be set explicitly in the build system: .. literalinclude:: ../tutorial/advanced/advanced_example4.py - :lines: 21 + :lines: 17 :dedent: 8 Setting a Time Limit for Regression Tests diff --git a/docs/reference.rst b/docs/reference.rst index e181154921..1d170ab832 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -96,3 +96,32 @@ Modules System API ------------------ .. autoclass:: reframe.core.modules.ModulesSystem + + +Build systems +------------- + +.. versionadded:: 2.14 + +ReFrame delegates the compilation of the regression test to a `build system`. +Build systems in ReFrame are entities that are responsible for generating the necessary shell commands for compiling a code. +Each build system defines a set of attributes that users may set in order to customize their compilation. +An example usage is the following: + +.. code:: python + + self.build_system = 'SingleSource' + self.build_system.cflags = ['-fopenmp'] + +Users simply set the build system to use in their regression tests and then they configure it. +If no special configuration is need for the compilation, users may completely ignore the build systems. +ReFrame will automatically pick one based on the regression test attributes and will try to compile the code. + +All build systems in ReFrame derive from the abstract base class :class:`reframe.core.buildsystems.BuildSystem`. +This class defines a set of common attributes, such us compilers, compilation flags etc. that all subclasses inherit. +It is up to the concrete build system implementations on how to use or not these attributes. + +.. automodule:: reframe.core.buildsystems + :members: + :exclude-members: BuildSystemField + :show-inheritance: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0e92f4e64d..ccd1c1bd18 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -217,6 +217,113 @@ Notice how our regression test is run on every partition of the configured syste Now that you have got a first understanding of how a regression test is written in ReFrame, let's try to expand our example. +Inspecting the ReFrame Generated Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As described in the `regression test pipeline `__ section, ReFrame generates several files during the execution of a test. +When developing or debugging a regression test it is important to be able to locate them and inspect them. + +As soon as the `setup` stage of the test is executed, a stage directory specific to this test is generated. +All the required resources for the test are copied to this directory, and this will be the working directory for the compilation, running, sanity and performance checking phases. +If the test is successful, this stage directory is removed, unless the ``--keep-stage-files`` option is passed in the command line. +Before removing this directory, ReFrame copies the following files to a dedicated output directory for this test: + +- The generated build script and its standard output and standard error. + This allows you to inspect exactly how your test was compiled. +- The generated run script and its standard output and standard error. + This allows you to inspect exactly how your test was run and verify that the sanity checking was correct. +- Any other user-specified files. + +If a regression test fails, its stage directory will not be removed. +This allows you to reproduce exactly what ReFrame was trying to perform and will help you debug the problem with your test. + +Let's rerun our first example and instruct ReFrame to keep the stage directory of the test, so that we can inspect it. + +.. code:: + + ./bin/reframe -C tutorial/config/settings.py -c tutorial/example1.py -r --keep-stage-files + +ReFrame creates a stage directory for each test case using the following pattern: + +.. code:: + + $STAGEDIR_PREFIX//// + +Let's pick the test case for the ``gpu`` partition and the ``PrgEnv-gnu`` programming environment from our first test to inspect. +The default ``STAGEDIR_PREFIX`` is ``./stage``: + +.. code:: + + cd stage/daint/gpu/PrgEnv-gnu/Example1Test/ + +If you do a listing in this directory, you will see all the files contained in the ``tutorial/src`` directory, as well as the following files: + +.. code:: + + rfm_Example1Test_build.err rfm_Example1Test_job.err + rfm_Example1Test_build.out rfm_Example1Test_job.out + rfm_Example1Test_build.sh rfm_Example1Test_job.sh + + +The ``rfm_Example1Test_build.sh`` is the generated build script and the ``.out`` and ``.err`` are the compilation's standard output and standard error. +Here is the generated build script for our first test: + +.. code:: shell + + #!/bin/bash + + _onerror() + { + exitcode=$? + echo "-reframe: command \`$BASH_COMMAND' failed (exit code: $exitcode)" + exit $exitcode + } + + trap _onerror ERR + + module load daint-gpu + module unload PrgEnv-cray + module load PrgEnv-gnu + cc example_matrix_vector_multiplication.c -o ./Example1Test + + +Similarly, the ``rfm_Example1Test_job.sh`` is the generated job script and the ``.out`` and ``.err`` files are the corresponding standard output and standard error. +The generated job script for the test case we are currently inspecting is the following: + +.. code:: shell + + #!/bin/bash -l + #SBATCH --job-name="rfm_Example1Test_job" + #SBATCH --time=0:10:0 + #SBATCH --ntasks=1 + #SBATCH --output=/path/to/stage/daint/gpu/PrgEnv-gnu/Example1Test/rfm_Example1Test_job.out + #SBATCH --error=/path/to/stage/daint/gpu/PrgEnv-gnu/Example1Test/rfm_Example1Test_job.err + #SBATCH --constraint=gpu + module load daint-gpu + module unload PrgEnv-cray + module load PrgEnv-gnu + srun ./Example1Test 1024 100 + + +It is interesting to check here the generated job script for the ``login`` partition of the example system, which does not use a workload manager: + +.. code:: + + cat stage/daint/login/PrgEnv-gnu/Example1Test/rfm_Example1Test_job.sh + +.. code:: shell + + #!/bin/bash -l + module unload PrgEnv-cray + module load PrgEnv-gnu + ./Example1Test 1024 100 + + +This is one of the advantages in using ReFrame: +You do not have to care about the system-level details of the target system that your test is running. +Based on its configuration, ReFrame will generate the appropriate commands to run your test. + + Customizing the Compilation Phase --------------------------------- @@ -236,26 +343,50 @@ To define environment variables to be set during the execution of a test, you sh This is a dictionary, whose keys are the names of the environment variables and whose values are the values of the environment variables. Notice that both the keys and the values must be strings. -In order to set the compiler flags for the current programming environment, you have to override either the :func:`setup ` or the :func:`compile ` method of the :class:`RegressionTest `. -As described in `"The Regression Test Pipeline" `__ section, it is during the setup phase that a regression test is prepared for a new system partition and a new programming environment. -Here we choose to override the ``compile()`` method, since setting compiler flags is simply more relevant to this phase conceptually. +From version 2.14, ReFrame manages compilation of tests through the concept of build systems. +Any customization of the build process should go through a build system. +For straightforward cases, as in our first example, where no customization is needed, ReFrame automatically picks the correct build system to build the code. +In this example, however, we want to set the flags for compiling the OpenMP code. +Assuming our test supported only GCC, we could simply add the following lines in the ``__init__()`` method of our test: -.. note:: The :class:`RegressionTest ` implements the six phases of the regression test pipeline in separate methods. - Individual regression tests may override them to provide alternative implementations, but in all practical cases, only the :func:`setup ` and the :func:`compile ` methods may need to be overriden. - You will hardly ever need to override any of the other methods and, in fact, you should be very careful when doing it. +.. code:: python -The :attr:`current_environ ` attribute of the :class:`RegressionTest ` holds an instance of the current programming environment. -This variable is available to regression tests after the setup phase. Before it is :class:`None`, so you cannot access it safely during the initialization phase. -Let's have a closer look at the ``compile()`` method: + self.build_system = 'SingleSource' + self.build_system.cflags = ['-fopenmp'] + +The ``SingleSource`` build system that we use here supports the compilation of a single file only. +Each build system type defines a set of variables that the user can set. +Based on the selected build system, ReFrame will generate a build script that will be used for building the code. +The generated build script can be found in `the stage or the output directory of the test `__, along with the output of the compilation. +This way, you may reproduce exactly what ReFrame does in case of any errors. +More on the build systems feature can be found `here `__. + +Getting back to our test, simply setting the ``cflags`` to ``-fopenmp`` globally in the test will make it fail for programming environments other than ``PrgEnv-gnu``, since the OpenMP flags vary for the different compilers. +Ideally, we need to set the ``cflags`` differently for each programming environment. +To achieve this we need to override the :func:`setup ` method of the :class:`RegressionTest `. +As described in `"The Regression Test Pipeline" `__ section, it is during the setup phase that a regression test is prepared for a new system partition and a new programming environment. +The following lines show the overriden ``setup()`` method: .. literalinclude:: ../tutorial/example2.py - :lines: 23-34 + :lines: 24-34 :dedent: 4 -We first take the name of the current programming environment (``self.current_environ.name``) and we check it against the set of the known programming environments. -We then set the compilation flags accordingly. -Since our target file is a C program, we just set the ``cflags`` of the current programming environment. -Finally, we call the ``compile()`` method of the base class, in order to perform the actual compilation. +The current environment is passed as argument by the framework to the ``setup()`` method, so we differentiate the build system's flags based on its name. +Finally, we need call the ``setup()`` method of the base class, in order to perform the actual setup of the test. + +.. tip:: + + The :class:`RegressionTest ` implements the six phases of the regression test pipeline in separate methods. + Individual regression tests may override them to provide alternative implementations, but in most practical cases, only the :func:`setup ` may need to be overriden. + You will hardly ever need to override any of the other methods and, in fact, you should be very careful when doing it. + + +.. warning:: + Setting the compiler flags in the programming environment is now deprecated. + Users are advised to use the build systems feature instead. + + .. versionadded:: 2.14 + An alternative implementation using dictionaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -263,13 +394,14 @@ An alternative implementation using dictionaries Here we present an alternative implementation of the same test using a dictionary to hold the compilation flags for the different programming environments. The advantage of this implementation is that you move the different compilation flags in the initialization phase, where also the rest of the test's specification is, thus making it more concise. -The ``compile()`` method is now very simple: -it gets the correct compilation flags from the ``prgenv_flags`` dictionary and applies them to the current programming environment. +The ``setup()`` method is now very simple: +it gets the correct compilation flags from the ``prgenv_flags`` dictionary and applies them to the build system. .. literalinclude:: ../tutorial/example2.py - :lines: 1-4,37-64 + :lines: 1-4,37-64 -.. tip:: A regression test is like any other Python class, so you can freely define your own attributes. +.. tip:: + A regression test is like any other Python class, so you can freely define your own attributes. If you accidentally try to write on a reserved :class:`RegressionTest ` attribute that is not writeable, ReFrame will prevent this and it will throw an error. Running on Multiple Nodes @@ -300,7 +432,7 @@ So we remove this partition from the list of the supported systems. The most important addition to this check are the variables controlling the distributed execution: .. literalinclude:: ../tutorial/example3.py - :lines: 23-25 + :lines: 24-26 :dedent: 8 By setting these variables, we specify that this test should run with 8 MPI tasks in total, using two tasks per node. @@ -338,7 +470,7 @@ Let's start with the OpenACC regression test: The things to notice in this test are the restricted list of system partitions and programming environments that this test supports and the use of the :attr:`modules ` variable: .. literalinclude:: ../tutorial/example4.py - :lines: 14 + :lines: 15 :dedent: 8 The :attr:`modules ` variable takes a list of modules that should be loaded during the setup phase of the test. @@ -347,7 +479,7 @@ In this particular test, we need to load the ``craype-accel-nvidia60`` module, w It is also important to note that in GPU-enabled tests the number of GPUs for each node have to be specified by setting the corresponding variable :attr:`num_gpus_per_node `, as follows: .. literalinclude:: ../tutorial/example4.py - :lines: 15 + :lines: 16 :dedent: 8 The regression test for the CUDA code is slightly simpler: @@ -438,7 +570,7 @@ The are two new variables set in this test that basically enable the performance Let's have a closer look at each of them: .. literalinclude:: ../tutorial/example7.py - :lines: 18-21 + :lines: 20-23 :dedent: 8 The :attr:`perf_patterns ` attribute is a dictionary, whose keys are *performance variables* (i.e., arbitrary names assigned to the performance values we are looking for), and its values are *sanity expressions* that specify how to obtain these performance values from the output. @@ -450,7 +582,7 @@ When the framework obtains a performance value from the output of the test it se Let's go over the :attr:`reference ` dictionary of our example and explain its syntax in more detail: .. literalinclude:: ../tutorial/example7.py - :lines: 22-26 + :lines: 24-28 :dedent: 8 This is a special type of dictionary that we call ``scoped dictionary``, because it defines scopes for its keys. diff --git a/reframe/core/buildsystems.py b/reframe/core/buildsystems.py index fb015f79c0..cbe48e198d 100644 --- a/reframe/core/buildsystems.py +++ b/reframe/core/buildsystems.py @@ -6,19 +6,94 @@ class BuildSystem: + """The abstract base class of any build system. + + Concrete build systems inherit from this class and must override the + :func:`emit_build_commands` abstract function. + """ + + #: The C compiler to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the compiler defined in the current programming environment will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` cc = fields.StringField('cc', allow_none=True) + + #: The C++ compiler to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the compiler defined in the current programming environment will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` cxx = fields.StringField('cxx', allow_none=True) + + #: The Fortran compiler to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the compiler defined in the current programming environment will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` ftn = fields.StringField('ftn', allow_none=True) + + #: The CUDA compiler to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the compiler defined in the current programming environment will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` nvcc = fields.StringField('nvcc', allow_none=True) + + #: The C compiler flags to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the corresponding flags defined in the current programming environment + #: will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` cflags = fields.TypedListField('cflags', str, allow_none=True) - cxxflags = fields.TypedListField('cxxflags', str, allow_none=True) + + #: The preprocessor flags to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the corresponding flags defined in the current programming environment + #: will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` cppflags = fields.TypedListField('cppflags', str, allow_none=True) + + #: The C++ compiler flags to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the corresponding flags defined in the current programming environment + #: will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` + cxxflags = fields.TypedListField('cxxflags', str, allow_none=True) + + #: The Fortran compiler flags to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the corresponding flags defined in the current programming environment + #: will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` fflags = fields.TypedListField('fflags', str, allow_none=True) + + #: The linker flags to be used. + #: If set to :class:`None` and :attr:`flags_from_environ` is :class:`True`, + #: the corresponding flags defined in the current programming environment + #: will be used. + #: + #: :type: :class:`str` + #: :default: :class:`None` ldflags = fields.TypedListField('ldflags', str, allow_none=True) - # Set compiler and compiler flags from the programming environment - # - # :type: :class:`bool` - # :default: :class:`True` + + #: Set compiler and compiler flags from the current programming environment + #: if not specified otherwise. + #: + #: :type: :class:`bool` + #: :default: :class:`True` flags_from_environ = fields.BooleanField('flags_from_environ') def __init__(self): @@ -35,13 +110,20 @@ def __init__(self): @abc.abstractmethod def emit_build_commands(self, environ): - """Return a list of commands needed for building using this build system. + """Return the list of commands for building with this build system. The build commands may always assume to be issued from the top-level directory of the code that is to be built. :arg environ: The programming environment for which to emit the build - instructions. + instructions. + The framework passes here the current programming environment. + :type environ: :class:`reframe.core.environments.ProgEnvironment` + :raises: :class:`BuildSystemError` in case of errors when generating + the build instructions. + + .. note:: + This method is relevant only to developers of new build systems. """ def _resolve_flags(self, flags, environ, allow_none=True): @@ -93,9 +175,55 @@ def _ldflags(self, environ): class Make(BuildSystem): + """A build system for compiling codes using ``make``. + + The generated build command has the following form: + + .. code:: + + make [-f MAKEFILE] [-C SRCDIR] [-j N] CC='X' CXX='X' FC='X' NVCC='X' CPPFLAGS='X' CFLAGS='X' CXXFLAGS='X' FFLAGS='X' LDFLAGS='X' OPTIONS + + The compiler and compiler flags variables will only be passed if they are + not :class:`None`. + Their value is determined by the corresponding attributes of + :class:`BuildSystem`. + If you want to completely disable passing these variables to the ``make`` + invocation, you should make sure not to set any of the correspoding + attributes and set also the :attr:`BuildSystem.flags_from_environ` flag to + :class:`False`. + """ + + #: Append these options to the ``make`` invocation. + #: This variable is also useful for passing variables or targets to ``make``. + #: + #: :type: :class:`list[str]` + #: :default: ``[]`` options = fields.TypedListField('options', str) + + #: Instruct build system to use this Makefile. + #: This option is useful when having non-standard Makefile names. + #: + #: :type: :class:`str` + #: :default: :class:`None` makefile = fields.StringField('makefile', allow_none=True) + + #: The top-level directory of the code. + #: + #: This is set automatically by the framework based on the + #: :attr:`reframe.core.pipeline.RegressionTest.sourcepath` attribute. + #: + #: :type: :class:`str` + #: :default: :class:`None` srcdir = fields.StringField('srcdir', allow_none=True) + + #: Limit concurrency for ``make`` jobs. + #: This attribute controls the ``-j`` option passed to ``make``. + #: If not :class:`None`, ``make`` will be invoked as ``make -j + #: max_concurrency``. + #: Otherwise, it will invoked as ``make -j``. + #: + #: :type: integer + #: :default: :class:`None` max_concurrency = fields.IntegerField('max_concurrency', allow_none=True) def __init__(self): @@ -160,9 +288,71 @@ def emit_build_commands(self, environ): class SingleSource(BuildSystem): + """A build system for compiling a single source file. + + The generated build command will have the following form: + + .. code:: + + COMP CPPFLAGS XFLAGS SRCFILE -o EXEC LDFLAGS + + - ``COMP`` is the required compiler for compiling ``SRCFILE``. + This build system will automatically detect the programming language of + the source file and pick the correct compiler. + See also the :attr:`SingleSource.lang` attribute. + - ``CPPFLAGS`` are the preprocessor flags and are passed to any compiler. + - ``XFLAGS`` is any of ``CFLAGS``, ``CXXFLAGS`` or ``FFLAGS`` depending on + the programming language of the source file. + - ``SRCFILE`` is the source file to be compiled. + This is set up automatically by the framework. + See also the :attr:`SingleSource.srcfile` attribute. + - ``EXEC`` is the executable to be generated. + This is also set automatically by the framework. + See also the :attr:`SingleSource.executable` attribute. + - ``LDFLAGS`` are the linker flags. + + For CUDA codes, the language assumed is C++ (for the compilation flags) and + the compiler used is :attr:`BuildSystem.nvcc`. + + """ + + #: The source file to compile. + #: This is automatically set by the framework based on the + #: :attr:`reframe.core.pipeline.RegressionTest.sourcepath` attribute. + #: + #: :type: :class:`str` or :class:`None` srcfile = fields.StringField('srcfile', allow_none=True) + + #: The executable file to be generated. + #: + #: This is set automatically by the framework based on the + #: :attr:`reframe.core.pipeline.RegressionTest.executable` attribute. + #: + #: :type: :class:`str` or :class:`None` executable = fields.StringField('executable', allow_none=True) + + #: The include path to be used for this compilation. + #: + #: All the elements of this list will be appended to the + #: :attr:`BuildSystem.cppflags`, by prepending to each of them the ``-I`` + #: option. + #: + #: :type: :class:`list[str]` + #: :default: ``[]`` include_path = fields.TypedListField('include_path', str) + + #: The programming language of the file that needs to be compiled. + #: If not specified, the build system will try to figure it out + #: automatically based on the extension of the source file. + #: The automatically detected extensions are the following: + #: + #: - C: `.c`. + #: - C++: `.cc`, `.cp`, `.cxx`, `.cpp`, `.CPP`, `.c++` and `.C`. + #: - Fortran: `.f`, `.for`, `.ftn`, `.F`, `.FOR`, `.fpp`, `.FPP`, `.FTN`, + #: `.f90`, `.f95`, `.f03`, `.f08`, `.F90`, `.F95`, `.F03` and `.F08`. + #: - CUDA: `.cu`. + #: + #: :type: :class:`str` or :class:`None` lang = fields.StringField('lang', allow_none=True) def __init__(self): diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index b43e5509c2..1dfc9b654c 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -125,6 +125,20 @@ class RegressionTest: #: Support for Git repositories was added. sourcesdir = fields.StringField('sourcesdir', allow_none=True) + #: The build system to be used for this test. + #: If not specified, the framework will try to figure it out automatically + #: based on the value the :attr:`sourcepath`. + #: + #: This field may be set using either a string referring to a concrete build + #: system class name (see `build systems `__) + #: or an instance of :class:`reframe.core.buildsystems.BuildSystem`. + #: The former is the recommended way. + #: + #: + #: :type: :class:`str` or :class:`reframe.core.buildsystems.BuildSystem`. + #: :default: :class:`None`. + #: + #: .. versionadded:: 2.14 build_system = BuildSystemField('build_system', allow_none=True) #: List of shell commands to be executed before compiling. From d2683c06550d5f771a477e1450164bacac267e22 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 3 Sep 2018 15:17:41 +0200 Subject: [PATCH 3/3] Address PR comments --- docs/reference.rst | 2 +- reframe/core/buildsystems.py | 4 ++-- reframe/core/pipeline.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 1d170ab832..d126f86063 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -114,7 +114,7 @@ An example usage is the following: self.build_system.cflags = ['-fopenmp'] Users simply set the build system to use in their regression tests and then they configure it. -If no special configuration is need for the compilation, users may completely ignore the build systems. +If no special configuration is needed for the compilation, users may completely ignore the build systems. ReFrame will automatically pick one based on the regression test attributes and will try to compile the code. All build systems in ReFrame derive from the abstract base class :class:`reframe.core.buildsystems.BuildSystem`. diff --git a/reframe/core/buildsystems.py b/reframe/core/buildsystems.py index cbe48e198d..3f2bea1831 100644 --- a/reframe/core/buildsystems.py +++ b/reframe/core/buildsystems.py @@ -110,7 +110,7 @@ def __init__(self): @abc.abstractmethod def emit_build_commands(self, environ): - """Return the list of commands for building with this build system. + """Return the list of commands for building using this build system. The build commands may always assume to be issued from the top-level directory of the code that is to be built. @@ -181,7 +181,7 @@ class Make(BuildSystem): .. code:: - make [-f MAKEFILE] [-C SRCDIR] [-j N] CC='X' CXX='X' FC='X' NVCC='X' CPPFLAGS='X' CFLAGS='X' CXXFLAGS='X' FFLAGS='X' LDFLAGS='X' OPTIONS + make -j [N] [-f MAKEFILE] [-C SRCDIR] CC='X' CXX='X' FC='X' NVCC='X' CPPFLAGS='X' CFLAGS='X' CXXFLAGS='X' FFLAGS='X' LDFLAGS='X' OPTIONS The compiler and compiler flags variables will only be passed if they are not :class:`None`. diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 1dfc9b654c..00d7b5cf8b 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -127,7 +127,7 @@ class RegressionTest: #: The build system to be used for this test. #: If not specified, the framework will try to figure it out automatically - #: based on the value the :attr:`sourcepath`. + #: based on the value of :attr:`sourcepath`. #: #: This field may be set using either a string referring to a concrete build #: system class name (see `build systems `__)