| Date | Description |
| 8-Feb-16 | Original posting |
In this assignment you'll develop a multithreaded implementation of the heart electrophysiology simulator discussed in class and conduct experiments to assess performance. The assignment includes a brief report, and the requirements for your report are described at the end of this writeup.
Your grade will based on 4 factors: performance attained by your implementation; correctness; well-displayed results; descriptive comments in the code and in your repository.
In the spirit of friendly competition, post your performance results to the HW3 Piazza folder.
☞ Due Friday2/5/16 @ 9PM
‣ ‣ ‣ Required solution files: solve.cpp helper.cpp
This means that these files, containing your solution to the assignment, must be turned in by the deadline.
You may develop your code anywhere, but you must ensure that it compiles and runs correctly on Bang because that’s where we’ll be testing it. (If you'd like to develop your software on another machine, like a laptop, see these instructions in the FAQ section below.
In doing this assignment, it's your responsibility to understand the course rules for integrity of scholarship.
All assignments will be turned in using UCSD's GitLab. The turnin procedure is described below in the section "Turning in your assignment". You must follow these steps to get the starter code and to get set up to do assignment 3.
Refer to the link below for instructions on how to obtain the starter code to get started on your assignment
A3 Git Instructions
Simulations play an important role in science, medicine, and engineering. A cardiac electrophysiology simulator like the one we are using in this assignment can be used for clinical diagnostic and therapeutic purposes. To reduce computational costs, simplifying assumptions are often made to make the simulations feasible. In our clinical example, time may be of the essence in determining an appropriate life-saving treatment.
Cell simulators entail solving a coupled set of a equations: a system of Ordinary Differential Equations (ODEs) together with a Partial Differential Equation (PDE). If you are interested in learning more about cardiac modeling peruse www.labheart.org or thevirtualheart.org or visit the web page of the UCSD Cardiac Mechanics Research Group and the Simula Computational Cardiac Modeling Department
Our simulator models the propagation of electrical signals in the heart, and it incorporates a cell model describing the kinetics of the membrane of a single cell. There can be different cell models, of varying degrees of complexity. We will use a model known as the Aliev-Panfilov model, that maintains 2 state variables, and solves one PDE (The PDE couples multiple cells into a system). This simple model can account for complex behavior such as how spiral waves break up and form elaborate patterns. Spiral waves can lead to life threatening situations such as ventricular fibrillation.
Our simulator models electrical signals in an idealized biological system in which voltages vary at discrete points in time, called timesteps, at the discrete positions of a mesh. The simulator is an iterative procedure that updates the simulation state variables (on the mesh) many times over a sequence of discrete timesteps. A simulation will run for some amount of simulated time, which is a real number. (For our purposes, the units of time are unimportant.) The timesteps subdivide the simulation time line into timesteps. The time line starts at t=0 and ends at some specified maximum time t=T. The unit of subdivision is called Δt, or the time discretization. It is important to keep in mind that when we use the term "time," we need to specify whether we are talking about real-valued or discrete simulation time. We will use the fully qualified name e.g. "discrete simulation time" to avoid ambiguity. By convention, we number the discrete timesteps on the simulation time line with integers. Thus, if we say that we are at timestep k of the simulation, then the current simulation time is kΔt.
To discretize space, we'll use a uniformly spaced two-dimensional mesh, shown in the left side of the figure
|
At each time step, the simulator updates the voltage according to nearest neighboring positions in space and time. This is done first in space, and next in time. Nearest neighbors in space are defined on the 4 Manhattan directions only: north, south, east and west, as shown on the right side of the figure.
In general, the finer the mesh — and hence the more numerous the points — the more accurate the solution, but at an increased computational cost. In a similar fashion, the smaller the timestep, the more accurate the solution, but at a higher computational cost. Thus, if h is the mesh spacing, Δt is the time discretization, then the running time is proportional to (Δt×h2)-1. However, to simplify our performance studies, we will run our simulations for a given number of timesteps (iterations) rather than a given amount of simulated time.
The simulator maintains two state variables characterizing the cell electrophysiology model: excitation (E) and recovery (R). Since we are using the method of finite differences to solve the problem, we discretize the variables E and R by considering the values only at a regularly spaced set of discrete points. We represent the variables as 2D arrays: E and R, respectively. (our notation will use a pseudo C syntax, E[i,j] is synonymous with E[i][j]) The PDE solver also maintains the voltage at the previous timestep, Eprev.
Here is the formula for solving the PDE (see solve.cpp), where the superscripts t and t-1 refer to the current and previous timestep, respectively (α is defined in apf.cpp):
Et[i,j] = Et-1[i,j] + α*(Et-1[i+1,j] + Et-1[i-1,j] + Et-1[i,j+1] + Et-1[i,j-1] - 4*Et-1[i,j])
Here are the formulas for solving the ODE:
E = E - Δt*(k*E * (E-a) * (E-1)+E * R);
R = R + Δt*(ε+ μ1*R / ( E+μ2)) * (-R-k*E * (E-b-1));
The variables k, a, b, ε, μ1 and μ2 are constant floating point scalars, and are defined in the simulator. References to E and R correspond to whole arrays. Expressions involving whole arrays (that is, array names without subscripts) are pointwise operations. We interpret a pointwise operation as follows. If A, B and C are 2D arrays of size N × N, then A = B + C is equivalent to the following loop nest, where A, B and C have the same size, N:
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
A[i][j] = B[i][j] + C[i][j];
Pointwise operations operations are parallelizable, as there is no possibility of any dependencies. They extend to mixed array expressions involving scalars. When whole array is combined with a scalar, e.g. (E-a), the scalar is extended to cover each element of the array, i.e. we compute the E[i][j] - a for all i and j.
To organize our work, we observe the following bookkeeping conventions. We divide each axis into N-1 subintervals using N points positioned uniformly along the line at positions x=i×h, i=0, 1, 2, ..., N-1. In order to properly handle updates next to a boundary, we pad the boundaries with a cell on each side. This padding will simplify the code (see comments about it in solve.cpp). This is shown in below, for 1 axis of the mesh drawn above, where the mesh spacing h = 1/(N-1). The points labeled 'o' correspond to the physical boundary, the points labeled 'P' are padding.
i=0 i=1 i=2 i=N-1 i=N i=N+1
P------o------|------| ...... |------|------|------o------P
^ ^
| |
x=0.0 x=h x=2h x=1.0-h x=1.0
Consider the case where we set N=64. We allocate an array of size 66 × 66, but we solve the problem on a central 64 × 64 submesh. Thus, our loops range from 1 to N, that is, 1 to 64. Points 0 and 65 correspond to padding points.
We've provided you with starter code that implements the Aliev-Panfilov solver as well as an executable so that you may compare your results with our reference. The file is found in $PUB/HW/A3/apf-ref on Bang. It may be possible to improve on the performance of the reference, since it doesn't include any significant optimizations to conserve memory locality.
All of your changes will made to just two files:
solve.cpp helper.cpp
Solve.cpp contains the solver, helper.cpp provides various helper functions such as storage allocation. (You may therefore change the storage allocation scheme if you wish.) The solver is computationally intensive, and so the focus of your parallelization and performance programming effort will be on this routine.
As with the previous assignments, build your code using the provided Makefile, which sets the appropriate flags for the compiler via the "arch" file located in $(PUB)/Arch/arch.gnu-c++11.generic. The Makefile produces a target called apf. Do not change the name of this target. While you may modify any files not listed above, remember that we will substitute fresh copies when we grade your assignment, so don't really on any custom changes as they will be lost during the grading process.
We have also provided one batch script, batch_job.sh. This script runs the serial provided code, and you'll need to modify it to run your multithreaded code.
You'll notice that the provided code uses a different naming convention for the bounds of the computational box. In particular, while we use the single letter N in this document, the code uses separate bounds for the x and y axes: the variables m and n. We need two variables so handle the rectangular subdomains that result from partitioning the data over multiple threads. You'll noticed that array indices are one-dimensional. We've "flattened" the logical two-dimensional arrays so that they are subscripted with just 1 index.
Also note the conventions used to index the 2D solution arrays: the indices are swapped, with the i index being the most rapidly varying index, that is, indexing along the last dimension of the array. Thus, array elements in the same row have different i indices and elements i-1, i and i+1 are adjacent in memory. Be careful to use the correct convention when writing flattened subscript expressions.
The simulator has various options, controlled via command line arguments (parsed in in cmdLine.cpp), with default values set in apf.cpp:
| -n <int> | Number of mesh points in the x and y dimensions |
| -i <int> | Duration of simulation in iterations |
| -s <int> | Print statistics, L2 and L∞ norms of the
excitation variable at specified regular intervals (in timesteps).
This feature is useful when debugging. |
| -p <int> | Plot the excitation variable, at the specified regular intervals (in timesteps). Useful when debugging. |
| -t <int> | the number of threads (Used only for your multithreaded implementation) |
| -d | Enable debugging output. Sets a variable called `debug` that you may use to debug your code. |
Thus, the following command line will conduct the simulation on a 150 × 150 box, run for 400 iterations, plot the excitation variable every 20 iterations, and print out summary values of the excitation variable every 10 iterations.
./apf -n 150 -i 400 -s -10 -p 20
If we leave off the -s and -p flags, then the output is as follows. The last line of output, beginning with the string "@" outputs performance data along with relevant parameters, and you can use this marker to extract performance data from an output file recording many runs.
$ ./apf -n 150 -i 400 [Simulation begins] Sat Feb 6 22:50:45 2016 dt= 0.0639292, # iters = 400 m x n = 150 x 150 # threads: 1 [Simulation ends] Sat Feb 6 22:50:45 2016 End at iteration 400 Max norm: 9.92837e-01, L2norm: 6.12580e-01 Running Time: 0.216509 sec. [1.16 GFlop/sec] M x N NT #iter T_p Gflops Linf, L2 @ 150 150 1 400 0.2165 1.164 9.92837e-01 6.12580e-01
The plotting capability, controlled via the -p flag, uses gnuplot, and may be useful in debugging. Do not enable plotting when you are measuring running times for your performance studies. The plotter capability only works on a single core and is discussed in more detail later in Part 4.
Your code will be tested for correctness and performance against various combinations of the N, niter and the number of threads. Test your code by comparing against the provided implementation. While you can catch some obvious errors using the graphical output (see below), a more precise test is to compare two summary quantities reported by the simulator: the L2 norm and the L∞ norm, the maximum value (magnitude) of the excitation variable, which are included in the 1 line of performance output. The first value is obtained by summing the squares of all solution values on the mesh, dividing by the number of points in the mesh (called normalization), and taking the square root of the result. For the second measurement, we take the "absolute max:" the maximum of the absolute value of each of the elements in the solution array. While there may be slight variations in the last digits of the reported quantities, variations in the first few digits indicate an error.
For example, you may find that after adding a new performance optimization that your code indeed ran faster but now the L2 errors differ in the 3rd decimal place, while the Max errors are the same. There is an error in your code. The L2 norm is giving a clearer picture of what is going on here; the maximum difference between the two solutions might not change, but the sum of the squares of their differences is another matter. Note, however, that the reverse situation could occur. The max norm may be the more useful measure. In this case your code is probably introducing small errors in many elements of the solution mesh, and a small number of larger errors. These errors aren't visible in the L2 norm, which takes an average. But the errors are exposed by the max norm. We give you both measures as they can each catch different kinds of errors. So be sure to check that each matches.
The code includes a plotting capability to enable you to visualize obvious errors (The summary statistics will be more useful than plotting for catching subtle errors). The simulations will produce a spiral pattern that evolves with time. However, if you set the number of iterations (-i) to a sufficiently large value, the organized spiral pattern will break up. This indicates non-physical behavior, and the length of time that the simulator produces physically meaningful results depends on the mesh size N. If you see this behavior occur for short lengths of time, or other non-physical behavior, you've located an error.
The plotter (Plotting.cpp) interfaces with gnuplot, as in Assignment 1. Remote visual display can be slow, so either work from a campus machine, or increase the frequency to at least 20 (-p 20). If a display window does not appear on your screen, then your ssh display settings are not set correctly. If you are connecting to the remote machine from a Linux platform using ssh, use the -X option:
$ ssh -X yourlogin@ccom-bang-login.ucsd.edu
If you are connecting from a Windows platform, and are using a 'secure shell client' AND 'exceed' (or xwin32, or cygwin) to log into a remote machine you must ensure that 'X11 tunneling' is set. To do this, you must have installed Cygwin /X with X11.
As in the previous assignments, use either an interactive node (via qlogin) or the non-interactive batch queue to collect timings.
Take timings using the provided timer code, which measures wall clock time via gettimeofday(), as in the previous assignments. Adjust the iteration count so that the running time never drops below 1 or 2 seconds.
When you are measuring running times for your performance studies, do not enable plotting (-p) nor output statistics using the -s option, as doing so could affect your observations.
The amount of work assigned to the most heavily and most lightly-loaded process along a given dimension must differ by not more than one row of the solution array.
This procedure is the same as the previous assignment and is reproduced here:
Git Lab Submission
A3-report-baden-powell.pdf. This report is worth 20% of your grade.
Provide all data in both tabular and graphical form. Plot your data carefully, plotting only what's necessary to get your points across, and choosing log and linear plots carefully. Be sure to provide a caption for each table or plot that describes the experiment you conducted, e.g. "strong scaling."
We encourage you to report negative results which are also valid especially valued if you can document them, with the underlying causes, or best guesses if the causes are not clear. So if results weren't as you expected, let us know why and we may be able to give you some partial credit. Include this discussion on the PDF containing your results.Don't wait until the last minute to attempt a turnin for the first time. Try out the turnin process early on so you are sure that you you are familiar with the process, and that all the required additional files are present.
You will receive 0 points for this assignment if your code does not compile, including cases of non-compilation due to merge conflicts. If you developed your code on another machine you are responsible for properly testing on Bang, else you may lose significant credit, possibly all 10% depending on the circumstances. As mentioned previously, you must ensure that you resolve all of your merge conflicts and that your code compiles and runs correctly IMMEDIATELY BEFORE you push to GitLab.
For this assignment staff will attempt intervene manually to fix problems. However, the policy for such manual intervention is as follows. Staff will change at most 3 lines of code, or you can send us the 3 changes yourself if asked. If such intervention is required we will assess a 10% penalty. This penalty is not severe and is an incentive to make small changes that fix a minor problem so that you can receive credit for all your hard work! Included in this policy are: coding errors, incorrectly naming your GitLab repository, or misspelling the words FINAL SUBMISSION in the commit message.
The only files you should be pushing to GitLab are text files: source code and ASCII test files. Do not push large binaries such as the executables or media files, as doing so could lead to problems such as losing the ability to push to GitLab. Staff will not intervene under these circumstances and you you may be unable to complete your assignment by the deadline. If you commit binaries, you are doing so at your own risk.
There are 25 possible points on the assignment and your grade will depend on 4 factors: performance attained by your code, correctness, well-displayed results (including an explanation if you have negative results) and comments. You must not only put comments inside your code, but every time you commit to your repository. There should be a steady stream of comments that enable us to retrace your development steps. Your performance score will be based on the ability to scale strongly.
We will grade your code on Bang using our own Makefile with the same compilation flags as in the provided in starter code. To get full credit, design, style and comments (including gitlab commit comments) count.
A. Copy over the "arch" file that the Makefile includes, and modify the path in the makefile to point at the place where you put the arch file. Make sure that you have g++ version 4.8.4. Earlier versions might also work (but not early than 4.7, which supports the C++11 standard) but not guaranteed. Before you turn in your code, be sure that it runs correctly on Bang and meets the performance goals on that machine If you are using an IDE you might need to add pthreads lib, as C++11 threads relies on pthreads. In some environments, this information may be needed.
A. Add asm=1 to the make command line as in
make asm=1 solve.oYou'll get a file called solve.cpp.lst, containing the assembler and the source code, with the assembler interleaved among the lines of source. This file is easier to read than the raw assembler output, but having said that, the optimizer makes life "interesting." Try turning the optimizer off to (-O0) the first time you try this out. To get the raw assembler output just do "make solve.s" For further information this blog: https://panthema.net/2013/0124-GCC-Output-Assembler-Code/ or this web site, http://gcc.godbolt.org/ that provides a graphical interface.
A. Here are a few tips.
[Mon Feb 8 21:24:27 PST 2016]