Combining Symbolic Execution and Model Checking to Verify MPI Programs
Hengbiao Yu, Zhenbang Chen, Xianjin Fu, Ji Wang, Zhendong Su, Jun Sun,, Chun Huang, and Wei Dong

TL;DR
This paper introduces MPI-SV, a novel tool that combines symbolic execution and model checking to efficiently verify MPI programs, especially those with non-blocking operations, significantly improving scalability and verification success rate.
Contribution
The paper presents MPI-SV, the first symbolic execution based verifier for MPI programs with non-blocking operations, integrating symbolic execution and model checking for enhanced verification capabilities.
Findings
MPI-SV verifies 90% of tasks compared to 55% by pure symbolic execution.
MPI-SV achieves 19x speedup in verifying properties.
MPI-SV verifies 100 tasks within one hour, outperforming previous methods.
Abstract
Message passing is the standard paradigm of programming in high-performance computing. However, verifying Message Passing Interface (MPI) programs is challenging, due to the complex program features (such as non-determinism and non-blocking operations). In this work, we present MPI symbolic verifier (MPI-SV), the first symbolic execution based tool for automatically verifying MPI programs with non-blocking operations. MPI-SV combines symbolic execution and model checking in a synergistic way to tackle the challenges in MPI program verification. The synergy improves the scalability and enlarges the scope of verifiable properties. We have implemented MPI-SV (footnote: https://mpi-sv.github.io) and evaluated it with 111 real-world MPI verification tasks. The pure symbolic execution-based technique successfully verifies 61 out of the 111 tasks (55\%) within one hour, while in comparison,…
Click any figure to enlarge with its caption.
Figure 1
Figure 2| Program | LOC | Brief Description |
|---|---|---|
| DTG | Dependence transition group | |
| Matmat | Matrix multiplication | |
| Integrate | Integral computing | |
| Diffusion2d | Simulation of diffusion equation | |
| Gauss_elim | Gaussian elimination | |
| Heat | Heat equation solver | |
| Mandelbrot | Mandelbrot set drawing | |
| Sorting | Array sorting | |
| Image_manip | Image manipulation | |
| DepSolver | Multimaterial electrostatic solver | |
| Kfray | KF-Ray parallel raytracer | |
| ClustalW | Multiple sequence alignment | |
| Total | 47354 | 12 open source programs |
| Program (#Procs) | T | Deadlock | Time(s) | #Iterations | ||
|---|---|---|---|---|---|---|
| Symbolic execution | Our approach | Symbolic execution | Our approach | |||
| DTG (5) | 0 | |||||
| 0 | ||||||
| 1 | ||||||
| 1 | ||||||
| 1 | ||||||
| 1 | ||||||
| Matmat∗(4) | o | 0 | 1 | |||
| Integrate (6/8/10) | 0/0/0 | | ||||
| 0/0/-1 | ||||||
| 1/1/1 | | |||||
| Integrate∗ (4/6) | o | 0/0 | ||||
| Diffusion2d (4/6) | 0/0 | 9.84/13.19 | 90/2041 | 1/1 | ||
| 0/1 | ||||||
| 0/1 | ||||||
| 0/0 | ||||||
| 1/1 | ||||||
| 1/0 | ||||||
| Gauss_elim (6/8/10) | 0/0/0 | 13.47/15.12/87.45 | 2756/2055/1662 | 1/1/1 | ||
| 1/1/1 | ||||||
| Heat (6/8/10) | 1/1/1 | |||||
| 1/1/1 | ||||||
| 1/1/1 | ||||||
| 1/1/1 | ||||||
| 1/1/1 | ||||||
| 1/1/1 | ||||||
| Mandelbrot (6/8/10) | 0/0/-1 | |||||
| -1/-1/-1 | ||||||
| -1/-1/-1 | | |||||
| 1/1/1 | ||||||
| Mandelbort∗ (4/6) | o | 0/0 | ||||
| Sorting∗ (4/6) | o | 0/0 | ||||
| Image_mani (6/8/10) | 0/0/0 | | ||||
| 1/1/1 | ||||||
| DepSolver (6/8/10) | 0/0/0 | | | |||
| Kfray (6/8/10) | 0/0/0 | |||||
| 1/1/1 | ||||||
| -1/-1/-1 | | |||||
| 1/1/1 | ||||||
| Kfray∗ (4/6) | o | 0/0 | ||||
| Clustalw (6/8/10) | 0/0/0 | 1234/1105/1162 | 1/1/1 | |||
| 0/0/0 | ||||||
| 0/0/0 | ||||||
| 1/1/1 | ||||||
| 0/0/0 | ||||||
| 0/0/0 | ||||||
Peer Reviews
No public reviews on file for this paper yet. If you reviewed it on a platform where reviews are public (OpenReview, ICLR, NeurIPS, ICML), you can paste yours below so the community can read it here.
Videos
No videos yet. Explain this paper in a talk, walkthrough, or lecture? Add one.
Taxonomy
TopicsSoftware Testing and Debugging Techniques · Radiation Effects in Electronics · Formal Methods in Verification
Combining Symbolic Execution and Model Checking to Verify MPI Programs
Hengbiao Yu1, Zhenbang Chen1, Xianjin Fu1,2, Ji Wang1,2, Zhendong Su3
and
Jun Sun4, Chun Huang1, Wei Dong1
1College of Computer, National University of Defense Technology, Changsha, China
2State Key Laboratory of High Performance Computing, National University of Defense Technology, Changsha, China
3Department of Computer Science, ETH Zurich, Switzerland
4School of Information Systems, Singapore Management University, Singapore
hengbiaoyu, zbchen, [email protected], [email protected], [email protected], [email protected]
(2020)
Abstract.
Message passing is the standard paradigm of programming in high-performance computing. However, verifying Message Passing Interface (MPI) programs is challenging, due to the complex program features (such as non-determinism and non-blocking operations). In this work, we present MPI symbolic verifier (MPI-SV), the first symbolic execution based tool for automatically verifying MPI programs with non-blocking operations. MPI-SV combines symbolic execution and model checking in a synergistic way to tackle the challenges in MPI program verification. The synergy improves the scalability and enlarges the scope of verifiable properties. We have implemented MPI-SV111MPI-SV is available https://mpi-sv.github.io. and evaluated it with 111 real-world MPI verification tasks. The pure symbolic execution-based technique successfully verifies 61 out of the 111 tasks (55%) within one hour, while in comparison, MPI-SV verifies 100 tasks (90%). On average, compared with pure symbolic execution, MPI-SV achieves 19x speedups on verifying the satisfaction of the critical property and 5x speedups on finding violations.
Symbolic Verification; Symbolic Execution; Model Checking; Message Passing Inteface; Synergy
††journalyear: 2020††copyright: acmcopyright††conference: ICSE ’20: 42nd International Conference on Software Engineering ; May 23-29, 2018; Seoul, South Korea††booktitle: ICSE ’20: ICSE ’20: 42nd International Conference on Software Engineering , May 23-29, 2020, Seoul, South Korea††price: 15.00††doi: XX.XXXX/XXXXXXX.XXXXXXX††isbn: xxx-x-xxxx-xxxx-x/xx/xx††ccs: Software and its engineering Software verification and validation
1. Introduction
Nowadays, an increasing number of high-performance computing (HPC) applications have been developed to solve large-scale problems (Buyya et al., 1999). The Message Passing Interface (MPI) (Snir, 1998) is the current de facto standard programming paradigm for developing HPC applications. Many MPI programs are developed with significant human effort. One of the reasons is that MPI programs are error-prone because of complex program features (such as non-determinism and asynchrony) and their scale. Improving the reliability of MPI programs is challenging (Gopalakrishnan et al., 2011, 2017).
Program analysis (Nielson et al., 2015) is an effective technique for improving program reliability. Existing methods for analyzing MPI programs can be categorized into dynamic and static approaches. Most existing methods are dynamic, such as debugging (Laguna et al., 2015), correctness checking (Samofalov et al., 2005) and dynamic verification (Vakkalanka et al., 2008). These methods need concrete inputs to run MPI programs and perform analysis based on runtime information. Hence, dynamic approaches may miss input-related program errors. Static approaches (Siegel, 2007; López et al., 2015; Bronevetsky, 2009; Botbol et al., 2017) analyze abstract models of MPI programs and suffer from false alarms, manual effort, and poor scalability. To the best of our knowledge, existing automated verification approaches for MPI programs either do not support input-related analysis or fail to support the analysis of the MPI programs with non-blocking operations, the invocations of which do not block the program execution. Non-blocking operations are ubiquitous in real-world MPI programs for improving the performance but introduce more complexity to programming.
Symbolic execution (King, 1976; Godefroid et al., 2005) supports input-related analysis by systematically exploring a program’s path space. In principle, symbolic execution provides a balance between concrete execution and static abstraction with improved input coverage or more precise program abstraction. However, symbolic execution based analyses suffer from path explosion due to the exponential increase of program paths w.r.t. the number of conditional statements. The problem is particularly severe when analyzing MPI programs because of parallel execution and non-deterministic operations. Existing symbolic execution based verification approaches (Siegel and Zirkel, 2011b)(Fu et al., 2015) do not support non-blocking MPI operations.
In this work, we present MPI-SV, a novel verifier for MPI programs by smartly integrating symbolic execution and model checking. As far as we know, MPI-SV is the first automated verifier that supports non-blocking MPI programs and LTL (Manna and Pnueli, 1992) property verification. MPI-SV uses symbolic execution to extract path-level models from MPI programs and verifies the models w.r.t. the expected properties by model checking (Clarke et al., 1999). The two techniques complement each other: (1) symbolic execution abstracts the control and data dependencies to generate verifiable models for model checking, and (2) model checking improves the scalability of symbolic execution by leveraging the verification results to prune redundant paths and enlarges the scope of verifiable properties of symbolic execution.
In particular, MPI-SV combines two algorithms: (1) symbolic execution of non-blocking MPI programs with non-deterministic operations, and (2) modeling and checking the behaviors of an MPI program path precisely. To safely handle non-deterministic operations, the first algorithm delays the message matchings of non-deterministic operations as much as possible. The second algorithm extracts a model from an MPI program path. The model represents all the path’s equivalent behaviors, i.e., the paths generated by changing the interleavings and matchings of the communication operations in the path. We have proved that our modeling algorithm is precise and consistent with the MPI standard (Forum, 2012). We feed the generated models from the second algorithm into a model checker to perform verification w.r.t. the expected properties, i.e., safety and liveness properties in linear temporal logic (LTL) (Manna and Pnueli, 1992). If the extracted model from a path satisfies the property , ’s equivalent paths can be safely pruned; otherwise, if the model checker reports a counterexample, a violation of is found. This way, we significantly boost the performance of symbolic execution by pruning a large set of paths which are equivalent to certain paths that have been already model-checked.
We have implemented MPI-SV for MPI C programs based on Cloud9 (Bucur et al., 2011) and PAT (Sun et al., 2009). We have used MPI-SV to analyze 12 real-world MPI programs, totaling 47K lines of code (LOC) (three are beyond the scale that the state-of-the-art MPI verification tools can handle), w.r.t. the deadlock freedom property and non-reachability properties. For the 111 deadlock freedom verification tasks, when we set the time threshold to be an hour, MPI-SV can complete 100 tasks, i.e., deadlock reported or deadlock freedom verified, while pure symbolic execution can complete 61 tasks. For the 100 completed tasks, MPI-SV achieves, on average, 19x speedups on verifying deadlock freedom and 5x speedups on finding a deadlock.
The main contributions of this work are:
- •
A synergistic framework combining symbolic execution and model checking for verifying MPI programs.
- •
A method for symbolic execution of non-blocking MPI programs with non-deterministic operations. The method is formally proven to preserve the correctness of verifying reachability properties.
- •
A precise method for modeling the equivalent behaviors of an MPI path, which enlarges the scope of the verifiable properties and improves the scalability.
- •
A tool for symbolic verification of MPI C programs and an extensive evaluation on real-world MPI programs.
2. Illustration
In this section, we first introduce MPI programs and use an example to illustrate the problem that this work targets. Then, we overview MPI-SV informally by the example.
2.1. MPI Syntax and Motivating Example
MPI implementations, such as MPICH (Gropp, 2002) and OpenMPI (Gabriel et al., 2004), provide the programming interfaces of message passing to support the development of parallel applications. An MPI program can be implemented in different languages, such as C and C++. Without loss of generality, we focus on MPI programs written in C. Let be a set of types, a set of names, and a set of expressions. For simplifying the discussion, we define a core language for MPI processes in Figure 1, where , , and . An MPI program is defined by a finite set of processes . For brevity, we omit complex language features (such as the messages in the communication operations and pointer operations) although MPI-SV does support real-world MPI C programs.
The statement declares a variable with type . The statement assigns the value of expression to variable . A process can be constructed from basic statements by using the composition operations including sequence, condition and loop. For brevity, we incorporate the key message passing operations in the syntax, where indicates the destination process’s identifier. These message passing operations can be blocking or non-blocking. First, we introduce blocking operations:
- •
Ssend(e): sends a message to the eth process, and the sending process blocks until the message is received by the destination process.
- •
Send(e): sends a message to the eth process, and the sending process blocks until the message is copied into the system buffer.
- •
Recv(e): receives a message from the eth process, and the receiving process blocks until the message from the eth process is received.
- •
Recv(*): receives a message from any process, and the receiving process blocks until a message is received regardless which process sends the message.
- •
Barrier: blocks the process until all the processes have called Barrier.
- •
Wait(r): the process blocks until the operation indicated by r is completed.
A Recv(*) operation, called wildcard receive, may receive a message from different processes under different runs, resulting in non-determinism. The blocking of a Send(e) operation depends on the size of the system buffer, which may differ under different MPI implementations. For simplicity, we assume that the size of the system buffer is infinite. Hence, each Send(e) operation returns immediately after being issued. Note that our implementation allows users to configure the buffer size. To improve the performance, the MPI standard provides non-blocking operations to overlap computations and communications.
- •
ISend(e,r): sends a message to the eth process, and the operation returns immediately after being issued. The parameter r is the handle of the operation.
- •
IRecv(e,r): receives a message from the eth process, and the operation returns immediately after being issued. IRecv(*,r) is the non-blocking wildcard receive.
The operations above are key MPI operations. Complex operations, such as MPI_Bcast and MPI_Gather, can be implemented by composing these key operations. The formal semantics of the core language is defined based on communicating state machines (CSM) (Brand and Zafiropulo, 1983). We define each process as a CSM with an unbounded receiving FIFO queue. For the sake of space limit, the formal semantics can be referred to (Yu et al., 2020).
An MPI program runs in many processes spanned across multiple machines. These processes communicate by message passing to accomplish a parallel task. Besides parallel execution, the non-determinism in MPI programs mainly comes from two sources: (1) inputs, which may influence the communication through control flow, and (2) wildcard receives, which lead to highly non-deterministic executions.
Consider the MPI program in Figure 2. Processes , and only send a message to and then terminate. For process , if input is not equal to ‘a’, receives a message from in a blocking manner; otherwise, uses a non-blocking wildcard receive to receive a message. Then, receives a message from . When is ‘a’ and IRecv(,req) receives the message from , a deadlock occurs, i.e., blocks at Recv(3), and all the other processes terminate. Hence, to detect the deadlock, we need to handle the non-determinism caused by the input and the wildcard receive IRecv(,req).
To handle non-determinism due to the input, a standard remedy is symbolic execution (King, 1976). However, there are two challenges. The first one is to systematically explore the paths of an MPI program with non-blocking and wildcard operations, which significantly increase the complexity of MPI programs. A non-blocking operation does not block but returns immediately, causing out-of-order completion. The difficulty in handling wildcard operations is to get all the possibly matched messages. The second one is to improve the scalability of the symbolic execution. Symbolic execution struggles with path explosion. MPI processes run concurrently, resulting in an exponential number of program paths w.r.t. the number of processes. Furthermore, the path space increases exponentially with the number of wildcard operations.
2.2. Our Approach
MPI-SV leverages dynamic verification (Vakkalanka et al., 2008) and model checking (Clarke et al., 1999) to tackle the challenges. Figure 3 shows MPI-SV’s basic framework. The inputs of MPI-SV are an MPI program and an expected property, e.g., deadlock freedom expressed in LTL. MPI-SV uses the built-in symbolic executor to explore the path space automatically and checks the property along with path exploration. For a path that violates the property, called a violation path, MPI-SV generates a test case for replaying, which includes the program inputs, the interleaving sequence of MPI operations and the matchings of wildcard receives. In contrast, for a violation-free path , MPI-SV builds a communicating sequential process (CSP) model , which represents the paths which can be obtained based on by changing the interleavings and matchings of the communication operations in . Then, MPI-SV utilizes a CSP model checker to verify w.r.t. the property. If the model checker reports a counterexample, a violation is found; otherwise, if satisfies the property, MPI-SV prunes all behaviors captured by the model so that they are avoided by symbolic execution.
Since MPI processes are memory independent, MPI-SV will select a process to execute in a round-robin manner to avoid exploring all interleavings of the processes. A process keeps running until it blocks or terminates. When encountering an MPI operation, MPI-SV records the operation instead of executing it and doing the message matching. When every process blocks or terminates and at least one blocked process exists, MPI-SV matches the recorded MPI operations of the processes w.r.t. the MPI standard (Forum, 2012). The intuition behind this strategy is to collect the message exchanges as thoroughly as possible, which helps find possible matchings for the wildcard receive operations. Consider the MPI program in Figure 2 and the deadlock freedom property. Figure 4 shows the symbolic execution tree, where the node labels indicate process communications, e.g., means that receives a message from . MPI-SV first symbolically executes , which only sends a message to . The Send(1) operation returns immediately with the assumption of infinite system buffers. Hence, terminates, and the operation Send(1) is recorded. Then, MPI-SV executes and explores both branches of the conditional statement as follows.
(1) True branch ( ‘a’). In this case, blocks at Recv(0). MPI-SV records the receive operation for , and starts executing . Like , executes operation Send(1) and terminates, after which is selected and behaves the same as . After terminates, the global execution blocks, i.e., blocks and all the other processes terminate. When this happens, MPI-SV matches the recorded operations, performs the message exchanges and continues to execute the matched processes. The Recv(0) in should be matched with the Send(1) in . After executing the send and receive operations, MPI-SV selects to execute, because terminates. Then, blocks at Recv(3). Same as earlier, the global execution blocks and operation matching needs to be done. Recv(3) is matched with the Send(1) in . After executing the Recv(3) and Send(1) operations, all the processes terminate successfully. Path in Figure 4 is explored.
(2) False branch ( =‘a’). The execution of proceeds until reaching the blocking receive Recv(3). Additionally, the two issued receive operations, i.e., IRecv(,req) and Recv(3), are recorded. Similar to the true branch, when every process blocks or terminates, we handle operation matching. Here , and terminate, and blocks at Recv(3). IRecv(,req) should be matched first because of the non-overtaken policy in the MPI standard (Forum, 2012). There are three Send operation candidates from , and , respectively. MPI-SV forks a state for each candidate. Suppose MPI-SV first explores the state where IRecv(*,req) is matched with ’s Send(1). After matching and executing ’s Recv(3) and ’s Send(1), the path terminates successfully, which generates path in Figure 4.
Violation detection**.** MPI-SV continues to explore the remaining two cases. Without CSP-based boosting, the deadlock would be found in the last case (i.e., in Figure 4), where IRecv(,req) is matched with ’s Send(1) and blocks because Recv(3) has no matched operation. MPI-SV generates a CSP model based on the deadlock-free path where ’s IRecv(,req) is matched with ’s Send(1). Each MPI process is modeled as a CSP process, and all the CSP processes are composed in parallel to form . Notably, in , we collect the possible matchings of a wildcard receive through statically matching the arguments of operations in the path. Additionally, the requirements in the MPI standard, i.e., completes-before relations (Vakkalanka et al., 2008), are also modeled. A CSP model checker then verifies deadlock freedom for . The model checker reports a counterexample where IRecv(*,req) is matched with the Send(1) in . MPI-SV only explores two paths for detecting the deadlock and avoids the exploration of and (indicated by dashed lines).
Pruning**.** Because the CSP modeling is precise (cf. Section 4), in addition to finding violations earlier, MPI-SV can also perform path pruning when the model satisfies the property. Suppose we change the program in Figure 2 to be the one where the last statement of is a Recv(*) operation. Then, the program is deadlock free. The true branch ( ‘a’) has 2 paths, because the last wildcard receive in has two matchings (i.e., ’s send and ’s send, and ’s send has been matched by ’s Recv(0)). The false branch ( ‘a’) has 6 paths because the first wildcard receive has 3 matchings (send operations from , and ) and the last wildcard receive has 2 matchings (because the first wildcard receive has matched one send operation). Hence, in total, there are 8 paths (i.e., ) if we use pure symbolic execution. In contrast, with model checking, MPI-SV only needs 2 paths to verify that the program is deadlock-free. For each branch, the generated model is verified to be deadlock-free, so MPI-SV prunes the candidate states forked for the matchings of the wildcard receives.
Properties**.** Because our CSP modeling encodes the interleavings of the MPI operations in the MPI processes, the scope of the verifiable properties is enlarged, i.e., MPI-SV can verify safety and liveness properties in LTL. Suppose we change the property to be the one that requires the Send(1) operation in should be completed before the Send(1) operation in . Actually, the send operation in can be completed before the send operation in , due to the nature of parallel execution. However, pure symbolic execution fails to detect the property violation. In contrast, with the help of CSP modeling, when we verify the model generated from the first path w.r.t. the property, the model checker gives a counterexample, indicating that a violation of the property exists.
3. Symbolic Verification Method
In this section, we present our symbolic verification framework and then describe MPI-SV’s symbolic execution method.
3.1. Framework
Given an MPI program , a state in ’s symbolic execution is composed by the states of processes, i.e., , and each MPI process’s state is a 6-tuple , where maps each variable to a concrete value or a symbolic value, is the next program statement to execute, is the process’s path constraint (King, 1976), is the flag of process status belonging to , and are infinite buffers for storing the issued MPI operations not yet matched and the matched MPI operations, respectively. We use to denote that is a process state in the global state . An element of can be accessed by , e.g., is the th process’s status flag. In principle, a statement execution in any process advances the global state, making ’s state space exponential to the number of processes. We use variable defined in to record the sequence of the issued MPI operations in , and to denote the set of global state . Global state ’s path condition (denoted by ) is the conjunction of the path conditions of ’s processes, i.e., .
Algorithm 1 shows the details of MPI-SV. We use to store the global states to be explored. Initially, only contains , composed of the initial states of all the processes, and each process’s status is . At Line 1, Select picks a state from as the one to advance. Hence, Select can be customized with different search heuristics, e.g., depth-first search (DFS). Then, Scheduler selects an active process to execute. Next, Execute (cf. Algorithm 2) symbolically executes the statement in , and may add new states into . This procedure continues until is empty (i.e., all the paths have been explored), detecting a violation or time out (omitted for brevity). After executing , if all the processes in the current global state terminate, i.e., a violation-free path terminates, we use Algorithm 4 to generate a CSP model from the current state (Line 1). Then, we use a CSP model checker to verify w.r.t. . If satisfies (denoted by ), we prune the global states forked by the wildcard operations along the current path (Line 1), i.e., the states in whose path conditions imply ’s path condition; otherwise, if the model checker gives a counterexample, we report the violation and exit (Line 1).
Since MPI processes are memory independent, we employ partial order reduction (POR) (Clarke et al., 1999) to reduce the search space. Scheduler selects a process in a round-robin fashion from the current global state. In principle, Scheduler starts from the active MPI process with the smallest identifier, e.g., at the beginning, and an MPI process keeps running until it is blocked or terminated. Then, the next active process will be selected to execute. Such a strategy significantly reduces the path space of symbolic execution. Then, with the help of CSP modeling and model checking, MPI-SV can verify more properties, i.e., safety and liveness properties in LTL. The details of such technical improvements will be given in Section 4.
3.2. Blocking-driven Symbolic Execution
Algorithm 2 shows the symbolic execution of a statement. Common statements such as conditional statements are handled in the standard way (King, 1976) (omitted for brevity), and here we focus on MPI operations.
The main idea is to delay the executions of MPI operations as much as possible, i.e., trying to get all the message matchings. Instead of execution, Algorithm 2 records each MPI operation for each MPI process (Lines 2&2). We also need to update buffer after issuing an MPI operation (Lines 2&2). Then, if is a non-blocking operation, the execution returns immediately; otherwise, we block (Line 2, excepting the Wait of an ISend operation). When reaching GlobalBlocking (Lines 2&2), i.e., every process is terminated or blocked, we use Matching (cf. Algorithm 3) to match the recorded but not yet matched MPI operations and execute the matched operations. Since the opportunity of matching messages is GlobalBlocking, we call it blocking-driven symbolic execution.
Matching matches the recorded MPI operations in different processes. To obtain all the possible matchings, we delay the matching of a wildcard operation as much as possible. We use to match the non-wildcard operations first (Line 3) w.r.t. the rules in the MPI standard (Forum, 2012), especially the non-overtaken ones: (1) if two sends of a process send messages to the same destination, and both can match the same receive, the receive should match the first one; and (2) if a process has two receives, and both can match a send, the first receive should match the send. The matched send and receive operations will be executed, and the statuses of the involved processes will be updated to active, denoted by (Line 3). If there is no matching for non-wildcard operations, we use to match the wildcard operations (Line 3). For each possible matching of a wildcard receive, we fork a new state (denoted by at Line 3) to analyze each matching case. If no operations can be matched, but there exist blocked processes, a deadlock happens (Line 3). Besides, for the LTL properties other than deadlock freedom (such as temporal properties), we also check them during symbolic execution (omitted for brevity).
Take the program in Figure 5 for example. When all the processes block at Barrier, MPI-SV matches the recorded operation in the buffers of the processes, i.e., , , and . According to the MPI standard, each operation in the buffers is ready to be matched. Hence, Matching first matches the non-wildcard operations, i.e., the Barrier operations, then the status of each process becomes . After that, MPI-SV continues to execute the active processes and record issued MPI operations. The next GlobalBlocking point is: and terminate, and blocks at Wait(req2). The buffers are , , and , respectively. All the issued Wait operations are not ready to match, because the corresponding non-blocking operations are not matched. So Matching needs to match the wildcard operation, i.e., , which can be matched with or . Then, a new state is forked for each case and added to the .
Correctness. Blocking-driven symbolic execution is an instance of model checking with POR. We have proved the symbolic execution method is correct for reachability properties (Manna and Pnueli, 1992). Due to the space limit, the proof can be referred to (Yu et al., 2020).
4. CSP Based Path Modeling
In this section, we first introduce the CSP (Roscoe, 2005) language. Then, we present the modeling algorithm of an MPI program terminated path using a subset of CSP. Finally, we prove the soundness and completeness of our modeling.
4.1. CSP Subset
Let be a finite set of events, a set of channels, and a set of variables. Figure 6 shows the syntax of the CSP subset, where denotes a CSP process, , , and .
The single event process performs the event and terminates. There are three operators: sequential composition (), external choice () and parallel composition with synchronization (). performs as or , and the choice is made by the environment. Let be a finite set of processes, denotes the external choice of all the processes in . performs and in an interleaving manner, but and synchronize on the events in . The process performs as after reading a value from channel and writing the value to variable . The process writes the value of to channel and then behaves as . Process skip terminates immediately.
4.2. CSP Modeling
For each violation-free program path, Algorithm 4 builds a precise CSP model of the possible communication behaviors by changing the matchings and interleavings of the communication operations along the path.
The basic idea is to model the communication operations in each process as a CSP process, then compose all the CSP processes in parallel to form the model. To model , we scan its operation sequence in reverse. For each operation, we generate its CSP model and compose the model with that of the remaining operations in w.r.t. the semantics of the operation and the MPI standard (Forum, 2012). The modeling algorithm is efficient, and has a polynomial time complexity w.r.t. the total length of the recorded MPI operation sequences.
We use channel operations in CSP to model send and receive operations. Each send operation has its own channel, denoted by . We use a zero-sized channel to model Ssend operation (Line 4), because Ssend blocks until the message is received. In contrast, considering a Send or ISend operation is completed immediately, we use one-sized channels for them (Line 4), so the channel writing returns immediately. The modeling of Barrier (Line 4) is to generate a synchronization event that requires all the parallel CSP processes to synchronize it (Lines 4&4). The modeling of receive operations consists of three steps. The first step calculates the possibly matched channels written by the send operations (Lines 4&4). The second uses the external choice of reading actions of the matched channels (Lines 4&4), so as to model different cases of the receive operation. Finally, the refined external choice process is composed with the remaining model. If the operation is blocking, the composition is sequential (Line 4); otherwise, it is a parallel composition (Line 4).
(Lines 4&4) returns the set of the channels written by the possibly matched send operations of the receive operation . We scan to obtain the possibly matched send operations of . Given a receive operation in process , calculated as follows denotes the set of the matched send operations of .
- •
If is or , contains ’s send operations with as the destination process.
- •
If is or , contains any process’s send operations with as the destination process.
over-approximates ’s precisely matched operations, and can be optimized by removing the send operations that are definitely executed after ’s completion, and the ones whose messages are definitely received before ’s issue. For example, Let be Send(1);Barrier;Send(1), and be Recv();Barrier. SMO will add the two send operations in to the matching set of the Recv() in . Since Recv(*) must complete before , we can remove the second send operation in . Such optimization reduces the complexity of the CSP model. For brevity, we use to denote the optimized matching set. Then, is .
To satisfy the MPI requirements, (Lines 4&4) refines the models of receive operations by imposing the completes-before requirements (Vakkalanka et al., 2008) as follows:
- •
If a receive operation has multiple matched send operations from the same process, it should match the earlier issued one. This is ensured by checking the emptiness of the dependent channels.
- •
The receive operations in the same process should be matched w.r.t. their issue order if they receive messages from the same process, except the conditional completes-before pattern (Vakkalanka et al., 2008). We use one-sized channel actions to model these requirements.
We model a Wait operation if it corresponds to an IRecv operation (Line 4), because ISend operations complete immediately under the assumption of infinite system buffer. Wait operations are modeled by the synchronization in parallel processes. GenerateEvent generates a new synchronization event for each Wait operation (Line 4). Then, is produced after the corresponding non-blocking operation is completed (Line 4). The synchronization on ensures that a Wait operation blocks until the corresponding non-blocking operation is completed.
We use the example in Figure 5 for a demonstration. After exploring a violation-free path, the recorded operation sequences are ,
, . We first scan in reverse. Note that we don’t model Wait(req1), because it corresponds to ISend. We create a synchronization event B for modeling Barrier (Lines 4&4). For the ISend(1,req1), we model it by writing an element to a one-sized channel , and use prefix operation to compose its model with B (Lines 4-4). In this way, we generate CSP process (denoted by ) for . Similarly, we model by (denoted by ), where is also a one-sized channel and is a channel element. For , we generate a single event process to model Wait(req2), because it corresponds to IRecv (Lines 4-4). For IRecv(*,req2), we first compute the matched channels using SMO (Line 4), and contains both and . Then, we generate the following CSP process
[TABLE]
(denoted by ) for . Finally, we compose the CSP processes using the parallel operator to form the CSP model (Line 4), i.e., .
CSP modeling supports the case where communications depend on message contents. MPI-SV tracks the influence of a message during symbolic execution. When detecting that the message content influences the communications, MPI-SV symbolizes the content on-the-fly. We specially handle the widely used master-slave pattern for dynamic load balancing (Gropp et al., 2014). The basic idea is to use a recursive CSP process to model each slave process and a conditional statement for master process to model the communication behaviors of different matchings. We verified five dynamic load balancing MPI programs in our experiments (cf. Section 5.4). The details for supporting master-slave pattern is in Appendix A.3.
4.3. Soundness and Completeness
In the following, we show that the CSP modeling is sound and complete. Suppose GenerateCSP() generates the CSP process . Here, soundness means that models all the possible behaviors by changing the matchings or interleavings of the communication operations along the path to , and completeness means that each trace in represents a real behavior that can be derived from by changing the matchings or interleavings of the communications.
Since we compute by statically matching the arguments of the recorded operations, may contain some false matchings. Calculating the precisely matched operations of is NP-complete (Forejt et al., 2014), and we suppose such an ideal method exists. We use and to denote the generated models using and the ideal method, respectively. The following theorems ensure the equivalence of the two models under the stable-failure semantics (Roscoe, 2005) of CSP and ’s consistency to the MPI semantics, which imply the soundness and completeness of our CSP modeling method. Let denote the trace set (Roscoe, 2005) of CSP process , and denote the failure set of CSP process . Each element in is , where is a trace, and is the set of events refuses to perform after .
Theorem 4.1**.**
.
Proof.
We only give the skeleton of the proof. We first prove
[TABLE]
based on which we can prove . The main idea of proving these two equivalence relations is to use contradiction for proving the subset relations. We only give the proof of ; the other subset relations can be proved in a similar way.
Suppose there is a trace such that but . The only difference between and is that introduces more channel read operations during the modeling of receive operations. Hence, there must exist a read operation of an extra channel in . Suppose the first extra read is , where . Therefore, cannot be read in when the matching of the corresponding receive operation starts, but is not empty at in . Despite of the size of , there must exist a write operation in . Because is also a valid trace in , it means is not empty in at , which contradicts with the assumption that cannot be read in . Hence, holds. ∎
Theorem 4.2**.**
* is consistent with the MPI semantics.*
The proof’s main idea is to prove that is equal to the model defined by the formal MPI semantics (Yu et al., 2020) w.r.t. the failure divergence semantics. Then, based on Theorem 4.1, we can prove that is consistent with the MPI semantics. Please refer to (Yu et al., 2020) for the detailed proofs for these two theorems.
5. Experimental Evaluation
In this section, we first introduce the implementation of MPI-SV, then describes the research questions and the experimental setup. Finally, we give experimental results.
5.1. Implementation
We have implemented MPI-SV based on Cloud9 (Bucur et al., 2011), which is built upon KLEE (Cadar et al., 2008), and enhances KLEE with better support for POSIX environment and parallel symbolic execution. We leverage Cloud9’s support for multi-threaded programs. We use a multi-threaded library for MPI, called AzequiaMPI (Rico-Gallego and Martín, 2011), as the MPI environment model for symbolic execution. MPI-SV contains three main modules: program preprocessing, symbolic execution, and model checking. The program preprocessing module generates the input for symbolic execution. We use Clang to compile an MPI program to LLVM bytecode, which is then linked with the pre-compiled MPI library AzequiaMPI. The symbolic execution module is in charge of path exploration and property checking. The third module utilizes the state-of-the-art CSP model checker PAT (Sun et al., 2009) to verify CSP models, and uses the output of PAT to boost the symbolic executor.
5.2. Research Questions
We conducted experiments to answer the following questions:
- •
Effectiveness: Can MPI-SV verify real-world MPI programs effectively? How effective is MPI-SV when compared to the existing state-of-the-art tools?
- •
Efficiency: How efficient is MPI-SV when verifying real-world MPI programs? How efficient is MPI-SV when compared to the pure symbolic execution?
- •
Verifiable properties : Can MPI-SV verify properties other than deadlock freedom?
5.3. Setup
Table 1 lists the programs analyzed in our experiments. All the programs are real-world open source MPI programs. DTG is a testing program from (Vakkalanka, 2010). Matmat, Integrate and Diffusion2d come from the FEVS benchmark suite (Siegel and Zirkel, 2011a). Matmat is used for matrix multiplication, Integrate calculates the integrals of trigonometric functions, and Diffusion2d is a parallel solver for two-dimensional diffusion equation. Gauss_elim is an MPI implementation for gaussian elimination used in (Xue et al., 2009). Heat is a parallel solver for heat equation used in (Müller et al., 2011). Mandelbrot, Sorting and Image_manip come from github. Mandelbrot parallel draws the mandelbrot set for a bitmap, Sorting uses bubble sort to sort a multi-dimensional array, and Image_manip is an MPI program for image manipulations, e.g., shifting, rotating and scaling. The remaining three programs are large parallel applications. Depsolver is a parallel multi-material 3D electrostatic solver, Kfray is a ray tracing program creating realistic images, and ClustalW is a tool for aligning gene sequences.
To evaluate MPI-SV further, we mutate (Just et al., 2014) the programs by rewriting a randomly selected receive using two rules: (1) replace Recv(i) with ; (2) replace Recv() with . Here is an input variable, is a random value, and is generated randomly from the scope of the process identifier. The mutations for IRecv(i,r) and IRecv(,r) are similar. Rule 1 is to improve program performance and simplify programming, while rule 2 is to make the communication more deterministic. Since communications tend to depend on inputs in complex applications, such as the last three programs in Table 1, we also introduce input related conditions. For each program, we generate five mutants if possible, or generate as many as the number of receives. We don’t mutate the programs using master-slave pattern (Gropp et al., 2014), i.e., Matmat and Sorting, and only mutate the static scheduling versions of programs Integrate, Mandelbrot, and Kfray.
Baselines. We use pure symbolic execution as the first baseline because: (1) none of the state-of-the-art symbolic execution based verification tools can analyze non-blocking MPI programs, e.g., CIVL (Luo et al., 2017; Siegel et al., 2015); (2) MPI-SPIN (Siegel, 2007) can support input coverage and non-blocking operations, but it requires building models of the programs manually; and (3) other automated tools that support non-blocking operations, such as MOPPER (Forejt et al., 2014) and ISP (Vakkalanka et al., 2008), can only verify programs under given inputs. MPI-SV aims at covering both the input space and non-determinism automatically. To compare with pure symbolic execution, we run MPI-SV under two configurations: (1) Symbolic execution, i.e., applying only symbolic execution for path exploration, and (2) Our approach, i.e., using model checking based boosting. Most of the programs run with 6, 8, and 10 processes, respectively. DTG and Matmat can only be run with 5 and 4 processes, respectively. For Diffusion and the programs using the master-slave pattern, we only run them with 4 and 6 processes due to the huge path space. We use MPI-SV to verify deadlock freedom of MPI programs and also evaluate 2 non-reachability properties for Integrate and Mandelbrot. The timeout is one hour. There are three possible verification results: finding a violation, no violation, or timeout. We carry out all the tasks on an Intel Xeon-based Server with 64G memory and 8 2.5GHz cores running a Ubuntu 14.04 OS. We ran each verification task three times and use the average results to alleviate the experimental errors. To evaluate MPI-SV’s effectiveness further, we also directly compare MPI-SV with CIVL (Luo et al., 2017; Siegel et al., 2015) and MPI-SPIN (Siegel, 2007). Note that, since MPI-SPIN needs manual modeling, we only use MPI-SV to verify MPI-SPIN’s C benchmarks w.r.t. deadlock freedom.
5.4. Experimental Results
Table 2 lists the results for evaluating MPI-SV against pure symbolic execution. The first column shows program names, and #Procs is the number of running processes. T specifies whether the analyzed program is mutated, where denotes the original program, and represents a mutant. A task comprises a program and the number of running processes. We label the programs using master-slave pattern with superscript “*”. Column Deadlock indicates whether a task is deadlock free, where 0, 1, and -1 denote no deadlock, deadlock and unknown, respectively. We use unknown for the case that both configurations fail to complete the task. Columns Time(s) and #Iterations show the verification time and the number of explored paths, respectively, where to stands for timeout. The results where Our approach performs better is in gray background.
For the 111 verification tasks, MPI-SV completes 100 tasks () within one hour, whereas 61 tasks () for Symbolic execution. Our approach detects deadlocks in 48 tasks, while the number of Symbolic execution is 44. We manually confirmed that the detected deadlocks are real. For the 48 tasks having deadlocks, MPI-SV on average offers a 5x speedups for detecting deadlocks. On the other hand, Our approach can verify deadlock freedom for 52 tasks, while only 17 tasks for Symbolic execution. MPI-SV achieves an average 19x speedups. Besides, compared with Symbolic execution, Our approach requires fewer paths to detect the deadlocks (1/55 on average) and complete the path exploration (1/205 on average). These results demonstrate MPI-SV’s effectiveness and efficiency.
Figure 7 shows the efficiency of verification for the two configurations. The X-axis varies the time threshold from 5 minutes to one hour, while the Y-axis is the number of completed verification tasks. Our approach can complete more tasks than Symbolic execution under the same time threshold, demonstrating MPI-SV’s efficiency. In addition, Our approach can complete tasks in minutes, which also demonstrates MPI-SV’s effectiveness.
For some tasks, e.g., Kfray, MPI-SV does not outperform Symbolic execution. The reasons include: (a) the paths contain hundreds of non-wildcard operations, and the corresponding CSP models are huge, and thus time-consuming to model check; (b) the number of wildcard receives or their possible matchings is very small, and as a result, only few paths are pruned.
Comparison with CIVL**.** CIVL uses symbolic execution to build a model for the whole program and performs model checking on the model. In contrast, MPI-SV adopts symbolic execution to generate path-level verifiable models. CIVL does not support non-blocking operations. We applied CIVL on our evaluation subjects. It only successfully analyzed DTG. Diffusion2d could be analyzed after removing unsupported external calls. MPI-SV and CIVL had similar performance on these two programs. CIVL failed on all the remaining programs due to compilation failures or lack of support for non-blocking operations. In contrast, MPI-SV successfully analyzed 99 of the 140 programs in CIVL’s latest benchmarks. The failed ones are small API test programs for the APIs that MPI-SV does not support. For the real-world program floyd that both MPI-SV and CIVL can analyze, MPI-SV verified its deadlock-freedom under 4 processes in 3 minutes, while CIVL timed out after 30 minutes. The results indicate the benefits of MPI-SV’s path-level modeling.
Comparison with MPI-SPIN**.** MPI-SPIN relies on manual modeling of MPI programs. Inconsistencies may happen between an MPI program and its model. Although prototypes exist for translating C to Promela (Jiang and Jonsson, 2009), they are impractical for real-world MPI programs. MPI-SPIN’s state space reduction treats communication channels as rendezvous ones; thus, the reduction cannot handle the programs with wildcard receives. MPI-SV leverages model checking to prune redundant paths caused by wildcard receives. We applied MPI-SV on MPI-SPIN’s 17 C benchmarks to verify deadlock freedom, and MPI-SV successfully analyzed 15 automatically, indicating the effectiveness. For the remaining two programs, i.e., BlobFlow and Monte, MPI-SV cannot analyze them due to the lack of support for APIs. For the real-world program gausselim, MPI-SPIN needs 171s to verify that the model is deadlock-free under 5 processes, while MPI-SV only needs 27s to verify the program automatically. If the number of the processes is 8, MPI-SPIN timed out in 30 minutes, but MPI-SV used 66s to complete verification.
Temporal properties**.** We specify two temporal safety properties and for Integrate and Mandelbrot, respectively, where requires process one cannot receive a message before process two, and requires process one cannot send a message before process two. Both and can be represented by an LTL formula U , which requires event cannot happen before event . We verify Integrate and Mandelbrot under 6 processes. The verification results show that MPI-SV detects the violations of and , while pure symbolic execution fails to detect violations.
Runtime bugs**.** MPI-SV can also detect local runtime bugs. During the experiments, MPI-SV finds 5 unknown memory access out-of-bound bugs: 4 in DepSolver and 1 in ClustalW.
6. Related Work
Dynamic analyses are widely used for analyzing MPI programs. Debugging or testing tools (Wave, 2009; Allinea, 2002; Krammer et al., 2004; Samofalov et al., 2005; Hilbrich et al., 2012; Laguna et al., 2015; Mitra et al., 2014) have better feasibility and scalability but depend on specific inputs and running schedules. Dynamic verification techniques, e.g., ISP (Vakkalanka et al., 2008) and DAMPI (Vo et al., 2010), run MPI programs multiple times to cover the schedules under the same inputs. Böhm et al. (2016) propose a state-space reduction framework for the MPI program with non-deterministic synchronization. These approaches can detect the bugs depending on specific matchings of wildcard operations, but may still miss inputs related bugs. MPI-SV supports both input and schedule coverages, and a larger scope of verifiable properties. MOPPER (Forejt et al., 2014) encodes the deadlock detection problem under concrete inputs in a SAT equation. Similarly, Huang and Mercer (2015) use an SMT formula to reason about a trace of an MPI program for deadlock detection. However, the SMT encoding is specific for the zero-buffer mode. Khanna et al. (2018) combines dynamic and symbolic analyses to verify multi-path MPI programs. Compared with these path reasoning work in dynamic verification, MPI-SV ensures input space coverage and can verify more properties, i.e., safety and liveness properties in LTL. Besides, MPI-SV employs CSP to enable a more expressive modeling, e.g., supporting conditional completes-before (Vakkalanka et al., 2008) and master-slave pattern (Gropp et al., 2014).
For static methods of analyzing MPI program, MPI-SPIN (Siegel, 2007, ) manually models MPI programs in Promela (Holzmann, 2012), and verifies the model w.r.t. LTL properties (Manna and Pnueli, 1992) by SPIN (Holzmann, 1997) (cf. Section 5.4 for empirical comparison). MPI-SPIN can also verify the consistency between an MPI program and a sequential program, which is not supported by MPI-SV. Bronevetsky (2009) proposes parallel control flow graph (pCFG) for MPI programs to capture the interactions between arbitrary processes. But the static analysis using pCFG is hard to be automated. ParTypes (López et al., 2015) uses type checking and deductive verification to verify MPI programs against a protocol. ParTypes’s verification results are sound but incomplete, and independent with the number of processes. ParTypes does not support non-deterministic or non-blocking MPI operations. MPI-Checker (Droste et al., 2015) is a static analysis tool built on Clang Static Analyzer (Clang, 2016), and only supports intraprocedural analysis of local properties such as double non-blocking and missing wait. Botbol et al. (2017) abstract an MPI program to symbolic transducers, and obtain the reachability set based on abstract interpretation (Cousot and Cousot, 1977), which only supports blocking MPI programs and may generate false positives. COMPI (Li et al., 2018; Li et al., 2019) uses concolic testing (Godefroid et al., 2005; Sen et al., 2005) to detect assertion or runtime errors in MPI applications. Ye et al. (2018) employs partial symbolic execution (Ramos and Engler, 2015) to detect MPI usage anomalies. However, these two symbolic execution-based bug detection methods do not support the non-determinism caused by wildcard operations. Luo and Siegel (2018) propose a preliminary deductive method for verifying the numeric properties of MPI programs in an unbounded number of processes. However, this method still needs manually provided verification conditions to prove MPI programs.
MPI-SV is related to the existing work on symbolic execution (King, 1976), which has been advanced significantly during the last decade (Godefroid et al., 2005; Sen et al., 2005; Cadar et al., 2008; Bucur et al., 2011; Godefroid et al., 2008; Pasareanu et al., 2008; Tillmann and de Halleux, 2008; Wang et al., 2018; Zhang et al., 2015). Many methods have been proposed to prune paths during symbolic execution (Boonstoppel et al., 2008; Jaffar et al., 2013; Cui et al., 2013; Guo et al., 2015; Yu et al., 2018). The basic idea is to use the techniques such as slicing (Jhala and Majumdar, 2005) and interpolation (McMillan, 2005) to safely prune the paths. Compared with them, MPI-SV only prunes the paths of the same path constraint but different message matchings or operation interleavings. MPI-SV is also related to the work of automatically extracting session types (Ng and Yoshida, 2016) or behavioral types (Lange et al., 2018) for Go programs and verifying the extracted type models. These methods extract over-approximation models from Go programs, and hence are sound but incomplete. Compared with them, MPI-SV extracts path-level models for verification. Furthermore, there exists work of combining symbolic execution and model checking (Nori et al., 2009; Su et al., 2015; Daca et al., 2016). YOGI (Nori et al., 2009) and Abstraction-driven concolic testing (Daca et al., 2016) combine dynamic symbolic execution (Godefroid et al., 2005; Sen et al., 2005) with counterexample-guided abstraction refinement (CEGAR) (Clarke et al., 2000). MPI-SV focuses on parallel programs, and the verified models are path-level. MPI-SV is also related to the work of unbounded verification for parallel programs (Bouajjani and Emmi, 2012; Bouajjani et al., 2018; Bakst et al., 2017; von Gleissenthall et al., 2019). Compared with them, MPI-SV is a bounded verification tool and supports the verification of LTL properties. Besides, MPI-SV is related to the existing work of testing and verification of shared-memory programs (Musuvathi et al., 2008; Guo et al., 2015; Guo et al., 2018; Demsky and Lam, 2015; Huang et al., 2013; Huang and Huang, 2016; Chaki et al., 2004; Cimatti et al., 2011; Kragl et al., 2018; Inverso et al., 2015; Yin et al., 2018). Compared with them, MPI-SV concentrates on message-passing programs. Utilizing the ideas in these work for analyzing MPI programs is interesting and left to the future work.
7. Conclusion
We have presented MPI-SV for verifying MPI programs with both non-blocking and non-deterministic operations. By synergistically combining symbolic execution and model checking, MPI-SV provides a general framework for verifying MPI programs. We have implemented MPI-SV and extensively evaluated it on real-world MPI programs. The experimental results are promising demonstrate MPI-SV’s effectiveness and efficiency. The future work lies in several directions: (1) enhance MPI-SV to support more MPI operations, (2) investigate the automated performance tuning of MPI programs based on MPI-SV, (3) apply our synergistic framework to other message-passing programs.
Appendix A Appendix
A.1. Semantics of the Core MPI Language
Auxiliary Definitions. Before giving the MPI language’s semantics, we give some auxiliary definitions. Given an MPI program , and denote ’s send and receive operations222 and can denote both blocking and unblocking operations, and we omit the parameter for non-blocking ones for the sake of simplicity., respectively, where and . represents the set of all the MPI operations in , is the process identifier of operation , and indicates whether is a blocking operation.
Definition A.1.
MPI Process State. An MPI process’s state is a tuple , where maps a variable to its value, is the next program statement to execute, is the flag of process status and belongs to the set {, , }, and are infinite buffers to store the issued MPI operations not yet matched and the matched MPI operations, respectively.
An element of a process state can be accessed by , e.g., is the status of . The behavior of a process can be regarded as a sequence of statements, and we use to denote the index of operation in the sequence. An MPI program’s global state is composed by the states of the MPI processes, i.e., . An MPI program’s semantics is a labeled transition system defined below.
Definition A.2.
Labeled Transition System. A labeled transition system (LTS) of an MPI program is a quadruple , where is the set of global states, denotes the set of actions defined below, represents the set of transitions, and is the set of initial states.
Actions. The action set is , where represents the synchronization of Barrier, is the execution of Wait operation, denotes the matching of message send and deterministic receive, represents the matching of message send and wildcard receive, and stands for the issue of operation .
Transition Rules. We first give some definitions used by the transition rules. We use defined as follows to indicate whether operation is ready to be matched in state w.r.t. the MPI standard (Forum, 2012), where represents that operation is in the buffer of process state and .
- •
If is , can be matched if the waited operation has been matched, i.e., .
- •
If is , can be matched if there is no previously issued not yet matched, i.e.,
.
- •
If is , can be matched if the previously issued or has been matched, which can be formalized as .
- •
If is , can be matched if the previously issued has been matched, i.e., . It is worth noting the conditional completes-before pattern (Vakkalanka et al., 2008), i.e., operation followed by a , and the can complete first when the matched message is not from . We will give a condition later to ensure such relation.
Suppose and are MPI operations, represents whether and can be matched statically, and can be defined as , where is
. We use to denote the updates of the process state with an update operation sequence . The operation updates the process status w.r.t. as follows.
[TABLE]
represents adding MPI operation to buffer , while represents removing from and adding to . We use to denote the statement next to . We use = to define the conditional completes-before relation requirement, where is
.
Figure 8 shows four transition rules for MPI operations. For the sake of brevity, we omit the transition rules of local statements, which only update the mapping and the next statement to execute. Rule describes the transition of issuing an MPI operation, which requires the issuing process to be . After issuing the operation, the process status is updated, the next statement to execute becomes (omitted for the sake of spaces), and the issued operation is added to the buffer . Rule is about matchings of message send and receive. There are three required conditions to match a send to a receive: (1) both of them have been issued to the buffer and are ready to be matched; (2) operation arguments are matched, i.e., ; (3) they comply with the conditional completes-before relation, i.e., . After matching, the matched operations will be removed from buffer and added to buffer , and the process status is updated. Rule is for barrier synchronization, which requires that all the processes have been blocked at the Barrier. After barrier synchronization, operation Barrier will be moved from buffer to buffer and all the processes become . Rule is for Wait operation, which requires the corresponding non-blocking operation has been finished. After executing Wait operation, the process becomes and the Wait operation will be removed from buffer and added to buffer .
A.2. Correctness of Symbolic Execution for MPI Programs
Round-robin schedule and blocking-driven symbolic execution is an instance of model checking with POR preserving reachability properties. Next, we prove the correctness of symbolic execution method for verifying reachability properties.
Definition A.3.
Reachability Property. A reachability property of an MPI program can be defined as follows, where represents an assertion of global state , e.g., deadlock and assertions of variables.
[TABLE]
returns true iff there exists an ’s state that satisfies the formula .
We use to represent the transition in the transition set and to denote the set of enabled actions at global state , i.e., . If , we use to represent . Instead of exploring all the possible states, MPI-SV only executes a subset of (denoted as ) when reaching a state . According to the workflow of MPI symbolic execution, we define below, where
- •
is the minimum process identifier that can issue an MPI operation.
- •
is the minimum process identifier of enabled non-wildcard actions. When is , is the process identifier of the send; when is , is the process identifier of the Wait operation.
[TABLE]
When is enabled in state , we will select the enabled issue operation having the smallest process identifier as , which is in accordance with round-robin schedule. Remember that blocking-driven matching delays the matching of wildcard receive () as late as possible. If is enabled, we use as ; else, we will use the deterministic matching ( or ) having smallest process identifier (we select in case ); otherwise, we use the enabled set as , i.e., .
Definition A.4.
Independence Relation (). For and , is a binary relation that if then , , and .
The dependence relation is the complement of , i.e., , if . Given an MPI program , whose LTS model is , an execution trace of is a sequence of actions, such that for each and . We use to represent the result state of , i.e., , and to represent is an execution trace of .
Definition A.5.
Execution equivalence is a reflexive and symmetric binary relation such that (1) . (2) if there exists a sequence that , and for every , where , , and is the concatenation of and .
Given an MPI program , suppose ’s semantic model is and the based model is , once satisfies conditions and , then for every execution trace , if , there exists an execution trace in that (Penczek et al., 2000).
, if , then for every trace , there exists , where .
On every cycle in , there exists at least one node that .
Before proving our selection of satisfies and , we first give a theorem to show the independence relation of co-enabled actions.
Theorem A.1**.**
Given action and action , for , if , then .
Proof.
We only prove the case that is . The proofs for the other three cases are similar. When and , can be , , , or w.r.t. the transition rules.
Suppose , , then . Since an issued operation can only block its process, we can have and . In addition,
. Hence, .
Suppose , , then . Since can only block process and can only make process active, and . In addition,
. Hence, .
Suppose , , , and . For : only updates the state , and and will not be affected. On the other hand, cannot make process blocked. Hence and . Then,
. For : since and are co-enabled, and is non-blocking. Due to the condition , has no effect on . On the other hand, since is non-blocking, cannot make process i blocked. Hence and . Additionally, since is non-blocking,
. For , the proof is similar. Hence . ∎
Theorem A.2**.**
* preserves the satisfaction of global reachability properties.*
Proof.
We first prove the satisfies condition and , respectively.
C1: , if , then for every trace , there exists , where .
Case 1: , C1 holds because .
Case 2: . In this case, contains only one element, and can be , , , or . Assume C1 does not hold, i.e., . According to Theorem A.1, and . Because , , which conflicts with the premise that . Hence C1 holds.
C2: Since there is no cycle in the labeled transition system of MPI programs, C2 holds.
Since , is a sub model of . Assume does not preserve the satisfaction of global reachability property . According to Definition A.3, if is , i.e., there exists a state in that satisfies , but no state in satisfying . Since satisfies C1 and C2, suppose the execution trace to is , i.e., , then there must exist an equivalent execution trace . Obviously, commuting independent actions cannot change the result state, so , which conflicts with the assumption. If is , i.e., each state in does not satisfy , but there exists a state in satisfying , which conflicts with is a sub model of . Hence the theorem holds.
∎
A.3. Support of Master-Slave Pattern
In real-world MPI programs, communications may depend on message contents, which makes the behavior more complicated. For example, suppose a message content is stored in variable , followed by “”. The general way to handle the situation where communications depend on the message contents is to make the contents symbolic. MPI-SV make to be symbolic once it detects that there exist communications depending on , so that MPI-SV does not miss a branch. Master-slave pattern is a representative situation and has been widely adopted to achieve a dynamic load balancing (Gropp et al., 1999). The verification of the parallel programs employing dynamic load balancing is a common challenge. In the pattern, the master process is in charge of management, i.e., dispatching jobs to slave processes and collecting results. On the other hand, the slave processes repeatedly work on the jobs and send results back to the master process until receiving the message of termination. Figure 9 is an example program. Following shows an example program.
is the master process, and the remaining processes are slaves. first dispatches one job to each slave. Then, will iteratively receive a job result from any slave (Recv(*,r)) and dispatch a new job to the slave process whose result is just received, i.e., Send(r.src), where r is the status of the receive and r.src denotes the process identifier of the received message. Each slave process iteratively receives a job (Recv(0)), completes the job (omitted for brevity) and sends the job result to (Send(0)). The if condition becomes true when the received message is for termination. The total number of jobs is controlled by the while loop in . After all the jobs have been completed, will notify all the slave processes to exit. The communication behavior of master-slave pattern is highly dynamic, i.e., the destination of the job send operation depends on the matching of the wildcard receive in the master process. In principle, if there are dynamically dispatched jobs for slaves, there exists cases of dispatching jobs. MPI-SV supports master-slave pattern as follows.
Recognition. We recognize the master-slave pattern automatically during symbolic execution. More precisely, to recognize the master process, we check whether the process identifier of a received message of a wildcard receive () is used as the destination of a send operation, i.e., . We call such a wildcard receive schedule receive. Then, we locate the corresponding slave processes w.r.t. the matchings of the schedule receive, i.e., a process is a slave if its message can match the schedule receive.
Modeling the master process. We allocate a global variable for each schedule receive to decide the destination process of the next job. In addition to the operations for modeling normal wildcard receive operations, i.e., using external choice of the matched channel readings, we write the read value from the matched channel to . For example, suppose and are the matched channels of a schedule receive, we model it by . Considering that a send operation in slave processes is modeled by writing the slave’s process identifier to the channel, we can use the value of to decide the destination of the next job.
Modeling the slave process. We use recursive CSP process to model a slave’s dynamic feature, i.e., repeatedly receiving a job and sending the result back until receiving the termination message. To model the job receive operation in a slave process, we use a guard expression before the channel reading of the receive operation, where is the global variable of the corresponding schedule receive and is the slave process’s identifier. Notably, the guard expression will disable the channel reading until the inside condition becomes true, indicating that the slave process cannot receive a new job unless its result has just been received by the master.
We need to refine the algorithms of symbolic execution and CSP modeling to support master-slave pattern. We have already implemented the refinement in MPI-SV. The support of master-slave pattern demonstrates that MPI-SV outperforms the single path reasoning work (Forejt et al., 2014; Huang and Mercer, 2015).
A.4. Proof of CSP Modeling’s Soundness and Completeness
Theorem 4.1 .
Proof.
We first prove , based on which we can prove .
First, we prove by contradiction. Suppose there exists a trace such that but . The only difference between and is that introduces more channel read operations during the modeling of receive operations. Hence, there must exist a read operation of an extra channel in . Suppose the first extra read is , where . Therefore, cannot be read in when the matching of the corresponding receive operation starts, but is not empty at in . Despite of the size of , there must exist a write operation in . Because is also a valid trace in , it means is not empty in at , which contradicts with the assumption that cannot be read in . Hence, holds.
Then, we prove also by contradiction. Suppose there exists a trace that but . Because is a superset of the precise matching set of , cannot be a terminated trace. So, blocks at . Because , there must exist a channel read operation that is enabled at in , i.e., is not empty. Hence, there must exist a write operation in . Because is valid in both of and , is also enabled at in , which contradicts with the assumption that blocks. Hence, we can have , and holds.
Next, we can prove in a similar way. Suppose there exists in but . It means there exists an event e in that is refused by at , but enabled by at . Because there is no internal choice in the CSP models, we have (Roscoe, 2005) and , which conflicts with . The contradiction of the case in which but
can be proved similarly.
Finally, is proved. ∎
Theorem 4.2 is consistent with the MPI semantics.
Proof.
If the global state of generating CSPstatic is , then we can get an MPI program from the sequence set , where each process of is the sequential composition of the operations in . Suppose the LTS model of is , and the LTS after hiding all the actions in is . Then, is consistent with the MPI semantics iff
is equal to , where is the event set of , and maps the events in the sequence and the set to the corresponding actions in MPI semantics, respectively. This can be shown by proving that Algorithms 4 with a precise ensures all the completes-before relations of MPI semantics (cf. semantic rules in Figure 8). The relations between send operations and those between receive operations (including conditional completes-before relation) are ensured by . The communications of and operations are modeled by CSP channel operations and process compositions. The requirements of Wait and Barrier operations are modeled by the process compositions defined in Algorithm 4. Hence, we can conclude that is consistent with the MPI semantics. Then, by Theorem 4.1, we can prove is consistent with the MPI semantics. ∎
The reference list from the paper itself. Each links out to its DOI / PubMed record.
- 1(1)
- 2Allinea (2002) Allinea. 2002. Allinea DDT. http://www.allinea.com/products/ddt/ . (2002).
- 3Bakst et al . (2017) Alexander Bakst, Klaus von Gleissenthall, Rami Gökhan Kici, and Ranjit Jhala. 2017. Verifying distributed programs via canonical sequentialization. PACMPL 1, OOPSLA (2017), 110:1–110:27.
- 4Böhm et al . (2016) Stanislav Böhm, Ondrej Meca, and Petr Jancar. 2016. State-Space Reduction of Non-deterministically Synchronizing Systems Applicable to Deadlock Detection in MPI. In FM . 102–118.
- 5Boonstoppel et al . (2008) Peter Boonstoppel, Cristian Cadar, and Dawson Engler. 2008. R Wset: attacking path explosion in constraint-based test generation. In TACAS . 351–366.
- 6Botbol et al . (2017) Vincent Botbol, Emmanuel Chailloux, and Tristan Le Gall. 2017. Static Analysis of Communicating Processes Using Symbolic Transducers. In VMCAI . 73–90.
- 7Bouajjani and Emmi (2012) Ahmed Bouajjani and Michael Emmi. 2012. Analysis of recursively parallel programs. In Proceedings of the 39th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2012, Philadelphia, Pennsylvania, USA, January 22-28, 2012 . 203–214.
- 8Bouajjani et al . (2018) Ahmed Bouajjani, Constantin Enea, Kailiang Ji, and Shaz Qadeer. 2018. On the Completeness of Verifying Message Passing Programs Under Bounded Asynchrony. In Computer Aided Verification - 30th International Conference, CAV 2018, Held as Part of the Federated Logic Conference, Flo C 2018, Oxford, UK, July 14-17, 2018, Proceedings, Part II . 372–391.
