|
| 1 | +--- |
| 2 | +title: "Testing parallel code" |
| 3 | +teaching: |
| 4 | +exercises: |
| 5 | +--- |
| 6 | + |
| 7 | +:::::::::::::::::::::::::::::::::::::: questions |
| 8 | + |
| 9 | +- How do I unit test a procedure which makes MPI calls? |
| 10 | +- How do I easily test different numbers of MPI ranks? |
| 11 | +- How do I test a procedure which uses OMP directives? |
| 12 | +- How do I easily test different numbers of OMP threads? |
| 13 | + |
| 14 | +:::::::::::::::::::::::::::::::::::::::::::::::: |
| 15 | + |
| 16 | +::::::::::::::::::::::::::::::::::::: objectives |
| 17 | + |
| 18 | +- Understand what is different when testing parallel vs serial code. |
| 19 | + - Assessment: Provide a test and/or a command to run a test for serial code and ask what needs to change to test parallel code |
| 20 | + - Assessment: Provide two versions of a code (serial and parallel) with a test for the serial version and ask learners to write a test for the parallel version. |
| 21 | + |
| 22 | +:::::::::::::::::::::::::::::::::::::::::::::::: |
| 23 | + |
| 24 | +## What's the difference? |
| 25 | + |
| 26 | +Depending on the parallelisation tool and strategy employed, the implementation of parallel code can be very different |
| 27 | +to that of serial code. This is especially true for code which utilises the message passing interface (MPI). These codes |
| 28 | +almost always contain some functionality in which processes, or ranks, communicate by exchanging messages. This message |
| 29 | +passing is often complex and will always benefit from testing. |
| 30 | + |
| 31 | +There is added complexity when testing MPI code compared to serial as the logical path through the code is changed |
| 32 | +depending on the number of ranks with which the code is executed. Therefore, it is important that we test for a range |
| 33 | +of numbers of ranks. This will require controlling the number of ranks running the src and is not something we want |
| 34 | +to implement ourselves. This limits the tools available to us. pFUnit is currently the only tool which supports |
| 35 | +testing MPI code. Therefore, we will be focusing on pFUnit for this section. |
| 36 | + |
| 37 | +## Tips for writing testable MPI code |
| 38 | + |
| 39 | +### Where possible, separate calls to the MPI library into units (subroutines or functions). |
| 40 | + |
| 41 | +If a procedure does not contain any calls to the MPI library, then it can be tested with a serial unit test. Therefore, |
| 42 | +separating MPI calls into their own units makes for a simpler test suite for most of your logic. Only, procedures with |
| 43 | +MPI library calls will require the more complex parallel pFUnit tests. |
| 44 | + |
| 45 | +### Pass the MPI communicator information into each procedure to be tested. |
| 46 | + |
| 47 | +If we pass the MPI communicator into a procedure, we can define this to be whatever we wish in our tests. This allows us |
| 48 | +to use the communicator provided by pFUnit or some other communicator specific to our problem. |
| 49 | + |
| 50 | +Creating types to wrap this information along with any other MPI specific information (neighbour ranks, etc) can be a |
| 51 | +convenient approach. |
| 52 | + |
| 53 | +## Syntax of writing MPI enabled pFUnit tests |
| 54 | + |
| 55 | +Firstly, we must change how we define our test parameters: |
| 56 | + |
| 57 | +- We now use `MPITestParameter` instead of `AbstractTestParameter`. |
| 58 | +- We can't know for certain the rank of each process for the pFUnit communicator until the test case runs. Therefore, we |
| 59 | + now need to build arrays of input parameters with the rank of a process matching the index of the parameter array. For |
| 60 | + example, rank 0 would access index 1 of the input array during testing, rank 1 would access index 2 and so on. See below |
| 61 | + for an example. |
| 62 | + |
| 63 | +```F90 |
| 64 | +@testParameter(constructor=new_exchange_boundaries_test_params) |
| 65 | +type, extends(MPITestParameter) :: my_test_params |
| 66 | + integer, allocatable :: input(:), expected_output(:) |
| 67 | +contains |
| 68 | + procedure :: toString => my_test_params_toString |
| 69 | +end type my_test_params |
| 70 | +``` |
| 71 | + |
| 72 | +We therefore need to update how we populate our test parameters to take into account the rank indexing: |
| 73 | + |
| 74 | +```F90 |
| 75 | +function my_test_suite() result(params) |
| 76 | + type(my_test_params), allocatable :: params(:) |
| 77 | + integer, allocatable :: input(:), expected_output(:) |
| 78 | + integer, max_number_of_ranks |
| 79 | +
|
| 80 | + max_number_of_ranks = 2 |
| 81 | + allocate(params(max_number_of_ranks)) |
| 82 | + allocate(input(max_number_of_ranks)) |
| 83 | + allocate(expected_output(max_number_of_ranks)) |
| 84 | +
|
| 85 | + ! Tests with one rank |
| 86 | + input(1) = 1 |
| 87 | + expected_output(1) = 2 |
| 88 | + params(1) = my_test_params(1, input, expected_output) |
| 89 | +
|
| 90 | + ! Tests with two ranks |
| 91 | + ! rank 0 |
| 92 | + input(1) = 1 |
| 93 | + expected_output(1) = 1 |
| 94 | + ! rank 1 |
| 95 | + input(2) = 1 |
| 96 | + expected_output(2) = 1 |
| 97 | + params(2) = my_test_params(2, input, expected_output) |
| 98 | +end function my_test_suite |
| 99 | +``` |
| 100 | + |
| 101 | +We also need to change how we define our test case: |
| 102 | + |
| 103 | +- We now use `MPITestCase` instead of `ParameterizedTestCase` |
| 104 | + |
| 105 | +```F90 |
| 106 | +@TestCase(testParameters={my_test_suite()}, constructor=my_test_params_to_my_test_case) |
| 107 | +type, extends(MPITestCase) :: my_test_case |
| 108 | + type(my_test_params) :: params |
| 109 | +end type my_test_case |
| 110 | +``` |
| 111 | + |
| 112 | +Finally, we ensure each process accesses the correct rank index parameters during the test |
| 113 | + |
| 114 | +```F90 |
| 115 | +@Test |
| 116 | +subroutine TestMySrcProcedure(this) |
| 117 | + class (my_test_case), intent(inout) :: this |
| 118 | +
|
| 119 | + integer :: actual_output, rank_index |
| 120 | +
|
| 121 | + rank_index = this%getProcessRank() + 1 |
| 122 | +
|
| 123 | + call my_src_procedure(this%params%input(rank_index), actual_output) |
| 124 | +
|
| 125 | + @assertEqual(this%params%expected_output(rank_index), actual_output, "Unexpected output from my_src_procedure") |
| 126 | +end subroutine TestMySrcProcedure |
| 127 | +``` |
| 128 | + |
| 129 | +::::::::::::::::::::::::::::::::::::: challenge |
| 130 | + |
| 131 | +### Challenge 1: Testing MPI parallel code |
| 132 | + |
| 133 | +Take a look at [5-testing-parallel-code/challenge-1](https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/5-testing-parallel-code/challenge-1) in the exercises repository. |
| 134 | + |
| 135 | +:::::::::::::::::::::::::::::::: solution |
| 136 | + |
| 137 | +A solution is provided in [episodes/5-testing-parallel-code/challenge-1/solution](https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/5-testing-parallel-code/challenge-1/solution). |
| 138 | + |
| 139 | +::::::::::::::::::::::::::::::::::::::::: |
| 140 | +:::::::::::::::::::::::::::::::::::::::::::::::: |
0 commit comments