Skip to content

Commit 915f9e1

Browse files
committed
Add testing parallel code lesson
1 parent 326fb2b commit 915f9e1

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ episodes:
7070
- 2-intro-to-fortran-unit-tests.md
7171
- 3-fortran-unit-test-syntax.md
7272
- 4-debugging-a-broken-test.md
73+
- 5-testing-parallel-code.md
7374

7475
# Information for Learners
7576
learners:
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 need to test for a
33+
range of numbers of ranks. This will require controlling the number of ranks running the src and is not something we
34+
want implement ourselves. This limits the tools available to us as 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+
54+
## Syntax of writing MPI enabled pFUnit tests
55+
56+
Firstly, we must change how we define our test parameters:
57+
58+
- We now use `MPITestParameter` instead of `AbstractTestParameter`.
59+
- We can't know for certain the rank of each process for the pFUnit communicator until the test case runs. Therefore, we
60+
now need to build arrays of input parameters with the rank of a process matching the index of the parameter array. For
61+
example, rank 0 would access index 1 of the input array during testing, rank 1 would access index 2 and so on. See below
62+
for an example.
63+
64+
```F90
65+
@testParameter(constructor=new_exchange_boundaries_test_params)
66+
type, extends(MPITestParameter) :: my_test_params
67+
integer, allocatable :: input(:), expected_output(:)
68+
contains
69+
procedure :: toString => my_test_params_toString
70+
end type my_test_params
71+
```
72+
73+
We therefore need to update how we populate our test parameters to take into account the rank indexing:
74+
75+
```F90
76+
function my_test_suite() result(params)
77+
type(my_test_params), allocatable :: params(:)
78+
integer, allocatable :: input(:), expected_output(:)
79+
integer, max_number_of_ranks
80+
81+
max_number_of_ranks = 2
82+
allocate(params(max_number_of_ranks))
83+
allocate(input(max_number_of_ranks))
84+
allocate(expected_output(max_number_of_ranks))
85+
86+
! Tests with one rank
87+
input(1) = 1
88+
expected_output(1) = 2
89+
params(1) = my_test_params(1, input, expected_output)
90+
91+
! Tests with two ranks
92+
! rank 0
93+
input(1) = 1
94+
expected_output(1) = 1
95+
! rank 1
96+
input(2) = 1
97+
expected_output(2) = 1
98+
params(2) = my_test_params(2, input, expected_output)
99+
end function my_test_suite
100+
```
101+
102+
We also need to change how we define our test case:
103+
104+
- We now use `MPITestParameter` instead of `AbstractTestParameter`
105+
106+
```F90
107+
@TestCase(testParameters={my_test_suite()}, constructor=my_test_params_to_my_test_case)
108+
type, extends(MPITestCase) :: my_test_case
109+
type(my_test_params) :: params
110+
end type my_test_case
111+
```
112+
113+
Finally, we ensure each process accesses the correct rank index parameters during the test
114+
115+
```F90
116+
@Test
117+
subroutine TestMySrcProcedure(this)
118+
class (my_test_case), intent(inout) :: this
119+
120+
integer :: actual_output, rank_index
121+
122+
rank_index = this%getProcessRank() + 1
123+
124+
call my_src_procedure(this%params%input(rank_index), actual_output)
125+
126+
@assertEqual(this%params%expected_output(rank_index), actual_output, "Unexpected output from my_src_procedure")
127+
end subroutine TestMySrcProcedure
128+
```
129+
130+
::::::::::::::::::::::::::::::::::::: challenge
131+
132+
### Challenge 1: Testing MPI parallel code
133+
134+
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.
135+
136+
:::::::::::::::::::::::::::::::: solution
137+
138+
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).
139+
140+
:::::::::::::::::::::::::::::::::::::::::
141+
::::::::::::::::::::::::::::::::::::::::::::::::

0 commit comments

Comments
 (0)