On the Scalability of the GPUexplore Explicit-State Model Checker
Nathan Cassee Thomas Neele Anton Wijs
cs.DC cs.DS

[
    [
    December 30, 2023
=====================




The use of graphics processors (GPUs) is a promising approach to speed up model checking to such an extent that it becomes feasible to instantly verify software systems during development. is an explicit-state model checker that runs all its computations on the GPU. Over the years it has been extended with various techniques, and the possibilities to further improve its performance have been continuously investigated. In this paper, we discuss how the hash table of the tool works, which is at the heart of its functionality. We propose an alteration of the hash table that in isolated experiments seems promising, and analyse its effect when integrated in the tool. Furthermore, we investigate the current scalability of , by experimenting both with input models of varying sizes and running the tool on one of the latest GPUs of NVIDIA.




§ INTRODUCTION


Model checking <cit.> is a technique to systematically determine whether a concurrent system adheres to desirable functional properties. There are numerous examples in which it has been successfully applied, however, the fact that it is computationally very demanding means that it is not yet a commonly used procedure in software engineering. Accelerating these computations with graphics processing units (GPUs) is one promising way to model check a system design in mere seconds or minutes as opposed to many hours.

 <cit.> is a model checker that performs all its computations on a GPU. Initially, it consisted of a state space exploration engine <cit.>, which was extended to perform on-the-fly deadlock and safety checking <cit.>.
Checking liveness properties has also been investigated <cit.>, with positive results, but liveness checking has yet to be integrated in the official release of the tool. Finally, in order to reduce the memory requirements of , partial order reduction has been successfully integrated <cit.>.

Since the first results achieved with  <cit.>, considerable progress has been made. For instance, the original version running on an NVIDIA K20 was able to explore the state space of the  model in approximately 72 minutes. With many improvements to 's algorithms, reported in <cit.>, the GPU hardware and the CUDA compiler, this has been reduced to 16 seconds. With these levels of speed-up, it has become much more feasible to interactively check and debug large models. Furthermore, GPU developments continue and many options can still be investigated. 

Performance is very important for a tool such as . However, so far, the scalability of the tool has not yet been thoroughly investigated. For instance, currently, we have access to a NVIDIA Titan X GPU, which is equipped with 12 GB global memory, but for all the models we have been using so far, 5 GB of global memory suffices as the models used for the run-time analysis of do not require more than 5GB for a state space exploration. In the current paper, we report on results we have obtained when scaling up models to utilise up to 12 GB.

In addition, we also experimentally compared running on a Titan X GPU with the Maxwell architecture, which was released on 2015, with running on a Titan X GPU with the Pascal architecture, which was released a year later. This provides insights regarding the effect recent hardware developments have on the tool.

Finally, we analyse the scalability of a critical part of the tool, namely its hash table. This structure is used during model checking to keep track of the progress made through the system state space. Even a small improvement of the hash table design may result in a drastic improvement in the performance of . Recently, we identified, by conducting isolated experiments, that there is still potential for further improvement <cit.>. In the current paper, we particularly investigate whether changing the size of the so-called buckets, i.e., data structures that act as containers in which states are stored, can have a positive effect on the running time.

The structure of the paper is as follows. In Section <ref>, we discuss related work. Next, an overview of the inner working of is presented in Section <ref>. The hash table and its proposed alterations are discussed in Section <ref>. After that, we present the results we obtained through experimentation in Section <ref>. Finally, conclusions and pointers to future work are given in Section <ref>.



§ RELATED WORK



In the literature, several different designs for parallel hash tables can be found. First of all, there is the hash table for GPUs proposed by Alcantara et al. <cit.>, which is based on Cuckoo hashing. Secondly, Laarman et al. <cit.> designed a hash table for multi-core shared memory systems. Their implementation was later used as a basis for the hash table underlying the LTSmin model checker. Other lock-free hash tables for the GPU are those proposed by Moazeni & Sarrafzadeh <cit.>, by Bordawekar <cit.> and by Misra & Chaudhuri <cit.>. Cuckoo hashing as implemented by Alcantara et al. is publicly available as part of the CUDPP library [<http://cudpp.github.io/>]. Unfortunately, to the best of our knowledge, there are no implementations available of the other hash table designs.

Besides  <cit.>, there are several other GPU model checking tools. Bartocci et al. <cit.> developed an extension for the SPIN model checker that performs state-space exploration on the GPU. They achieved significant speed-ups for large models.


A GPU extension to the parallel model checking tool DIVINE, called DIVINE-CUDA, was developed by Barnat et al. <cit.>. To speed-up the model checking process, they offload the cycle detection procedure to the GPU. Their tool can even benefit from the use of multiple GPUs. DIVINE-CUDA achieves a significant speed-up when model checking properties that are valid.

Edelkamp and Sulewski address the issues arising from the limited amount of global memory available on a GPU. In <cit.>, they implement a hybrid approach in a tool called CuDMoC, using the GPU for next state computation, while keeping the hash table in the main memory, to be accessed by multiple threads running on the Central Processing Unit (CPU). In <cit.>, they keep part of the state space in the global memory and store the rest on disk. The record on disk can be queried through a process they call delayed duplicate detection. Even though disk access causes overhead, they manage to achieve a speed-up over single-threaded tools.

Edelkamp et al. <cit.> also applied GPU programming to probabilistic model checking. They solve systems of linear equations on the GPU in order to accelerate the value iteration procedure. GPUs are well suited for this purpose, and can enable a speed-up of 18 times over a traditional CPU implementation.

Wu et al. <cit.> have developed a tool called GPURC that performs the full state-space exploration process on the GPU, similar to . Their implementation applies dynamic parallelism, a relatively new feature in CUDA that allows launching of new kernels from within a running kernel. Their tool shows a good speed-up compared to traditional single-threaded tools, although the added benefit of dynamic parallelism is minimal.

Finally, GPUs are also successfully applied to accelerate other computations related to model checking.
For instance, Wu et al. <cit.> use the GPU to construct counter-examples,
and state space decomposition and minimisation are investigated in <cit.>. For probabilistic model checking, Češka et al. <cit.> implemented GPU accelerated parameter synthesis for parameterized continous time Markov chains.



§ GPUS AND 





 <cit.> is an explicit-state model checker that practically runs entirely on a GPU (only the general progress is checked on the host side, i.e. by
a thread running on the CPU). It is written in CUDA C, an extension of C offered by Nvidia.
CUDA (Compute Unified Device Architecture) provides an interface to write applications for Nvidia's GPUs.
takes a network of Labelled Transition Systems (LTSs) <cit.> as input, and can construct the synchronous product of those LTSs using many threads in a Breadth-First-Search-based exploration, while optionally checking on-the-fly for the presence of deadlocks and violations of safety properties. A (negation of a) safety property can be added as an automaton to the network.

An LTS is a directed graph in which the nodes represent states and the edges are transitions between the states. Each transition has an action label representing an event leading from one state to another. An example network is shown in Figure <ref>, where the initial states are indicated by detached incoming arrows. One producer generates work and sends it to one of two consumers. This happens by means of synchronisation of the `send' and `rec' actions. The other actions can be executed independently. How the process LTSs should be combined using the relevant synchronisation rules is defined on the right in Figure <ref>, using the syntax of the Exp.open tool <cit.>. The state space of this network consists of 8 states and 24 transitions.

The general approach of to perform state space exploration is discussed in this section, leaving out many of the details that are not relevant for understanding the current work. The interested reader is
referred to <cit.>.

In a CUDA program, the host launches CUDA functions called kernels, that are to be executed many times in parallel by a specified number of GPU threads. Usually, all threads run the same kernel using different parts of the input data, although some GPUs allow multiple different kernels to be executed simultaneously (does not use this
feature). Each thread is executed by a streaming processor (SP). Threads are grouped in blocks of a predefined size. Each block is 
assigned to a streaming multiprocessor (SM). An SM consists of a fixed number of SPs (see Figure <ref>).



Each thread has a number of
on-chip registers that allow fast access. The threads in a block together share memory to exchange data, which is located in the (on-chip) shared memory of an SM.
Finally, the blocks can share data using the global memory of the GPU, which is relatively large, but slow, since it is off-chip.
The global memory is used to exchange data between the host and the kernel.
The GTX Titan X, which we used for our experiments, has 12 GB global memory and 24 SMs, each having 128 SPs (3,072 SPs in total).

Writing well-performing GPU applications is challenging, due to the execution model of GPUs, which is Single Instruction Multiple Threads.
Threads are partitioned in groups of 32 called warps. The threads in a warp run in lock-step, sharing a program counter, so they always execute the same program instruction.
Hence, thread divergence, i.e. the phenomenon of threads being forced to execute different instructions (e.g., due to if-then-else constructions) or to access physically distant
parts of the global memory, negatively affects performance.

Model checking tends to introduce divergences frequently, as it requires combining the behaviour of the processes in the network, and accessing and storing state vectors of the system state
space in the global memory. In , this is mitigated by combining relevant network information as much as possible in 32-bit integers, and storing these as textures, that only allow
read access and use a dedicated cache to speed up random accesses. Furthermore, in the global memory, a hash table is used to store state vectors (Figure <ref>). The currently used hash table has been designed to
optimise accesses of entire warps: the space is partitioned into buckets consisting of 32 integers, precisely enough for one warp to fetch a bucket with one combined memory access. State vectors
are hashed to buckets, and placed within a bucket in an available slot. If the bucket is full, another hash function is used to find a new bucket.
Each block accesses the global hash table to collect vectors that still require exploration.
To each state vector with n process states, a group of n threads is assigned to construct its
successors using fine-grained parallelism.
Since access to the global memory is slow, each block uses a dedicated state cache (Figure <ref>). It serves to store and collect newly produced state vectors, that are subsequently moved to the global hash table in batches. With the cache, block-local duplicates can be detected.



§ THE HASH TABLE



States discovered during the search exploration phase of are inserted into a global memory hash table. This hash table is used to keep track of the open and closed sets maintained during the breadth first search based exploration of the state space. 

Since many accesses (reads and writes) to the hash table are performed during state-space exploration, its performance is critical for our model checker. In order to allow for efficient parallel access, the hash table should be lock-free. To prevent corruption of state vectors, insertion should be an atomic operation, even when a state vector spans multiple 32 bit integers.


Given these requirements, we have considered several lock-free hash table implementations. One of them, proposed by Alcantara et al. <cit.>, uses so-called Cuckoo hashing. With Cuckoo hashing a key is hashed to a bucket in the hash table, and in case of a collision the key that is already in the bucket is evicted and rehashed using another hash function to a different bucket. Re-insertions continue until the last evicted key is hashed to an empty bucket, until all hash functions are exhausted or until the chain of re-insertions becomes too long <cit.>. The other hash table we considered is the one originally designed for  <cit.>; we refer to its hashing mechanism as hashing. We experimentally compared these two hash tables, and from these comparisons we concluded that while Cuckoo hashing on average performs better, it does not meet all the demands needed by . However, based on the performance evaluation a possible performance increase has been identified for hashing <cit.>. This section discusses the proposed modification, and its implementation.



 §.§ Hashing 




The hash table consists of two parts: the storage used to store the discovered vectors and the set of hash constants used by the hash function to determine the position of a vector in the hash table. The memory used to store vectors is divided into buckets with a size of 32 integers. Each bucket is additionally split into two equally sized half buckets. Therefore a single bucket can store 2 * *16/ vector_length vectors.
The reason for writing vectors to buckets with half-warps (a group of 16 threads) is that in many cases, atomic writes of half-warps are scheduled in an uninterrupted sequence <cit.>. This results in vectors consisting of multiple integers to be written without other write operations corrupting them.
It should be noted that the hash table uses closed hashing, i.e., the vectors themselves are stored in the hash table, as opposed to pointers to those vectors.


When inserting, a warp of 32 threads inserts a single vector into a bucket. The way threads are divided over a bucket can be observed in Figure <ref>, this figure visualizes a single bucket of the hash table for a vector length of 3. Each thread in a warp is assigned to one integer of the bucket, and to one integer of the vector. This assignment is done from left to right per half bucket. For this example the first 3 threads, i.e., the first vector group, is assigned to the first slot in the bucket, and the first thread in a vector group is assigned to the first integer of the vector. By assigning every thread to a single part of the vector and of the bucket each thread has a relatively simple task, which can be executed in parallel. 

The insertion algorithm first hashes the vector to insert to determine the bucket belonging to the vector. Each thread checks its place in the bucket and compares the integer on that position to the corresponding integer of the vector. After comparing, the insertion algorithm then uses CUDA warp instructions to quickly exchange data between all 32 threads in the warp to determine whether a single vector group of threads has found the vector. If the vector has been found the insertion algorithm terminates, if the vector has not been found the algorithm continues. 



Figure <ref> shows an example of the vector (8, 2, 1) being inserted into the hash table where the first two slots are already occupied. Because the first two slots are already occupied the first six threads compare their element of the vector to their element of the bucket. The icons above the arrows indicate the result of these comparisons. As can be seen there is one thread that has a match, however, because not all elements in the slot match the insertion algorithm does not report a find. 

If the vector is not found in the bucket, the insertion algorithm selects the first free slot in the bucket, in this case the third slot. This selection procedure can be done efficiently using CUDA warp instructions. Next, the associated threads attempt to insert the vector (8, 2, 1) into the selected slot using a compare and swap operation. If the insertion fails because another warp had claimed that slot already for another vector, the algorithm takes the next free slot in the bucket. 

If a bucket has no more free slots the next set of hash constants is used, and the next bucket is probed. This is repeated until all hash constants have been used, and if no insertion succeeds into any of the buckets, the insertion algorithm reports a failure and the exploration stops. 

In 2.0 the hash table is initialized using eight hash functions. After each exploration step, blocks that have found new vectors use the insertion algorithm to insert any new vectors they found into the global hash table. The hash table is therefore a vital part of . 

Buckets with a length of 32 integers have been chosen because of the fact that warps in CUDA consist of 32 threads. This way, every integer in a bucket can be assigned to a single thread. Besides, this design choice also allows for coalesced memory access: when a warp accesses a continuous block of 32 integers, this operation can be executed in a single memory fetch operation <cit.>. Uncoalesced accesses, on the other hand, have to be done individually after each other. By coalescing the accesses, the available bandwidth to global memory is used efficiently.



 §.§ Configurable Bucket Size


While the current implementation of the hash table makes it possible for to achieve a considerable increase in performance over CPU-based explicit-state model checkers <cit.>, it suffers from one disadvantage.
Namely, after initially scanning the bucket, only x threads, where x is the vector length, are active at a time. The other 32 - x threads are inactive while they await the result of the atomic insertion of the active group. If the insertion fails but there is still a free slot in the hash table, another group of x threads becomes active to attempt an atomic insertion, while the remaining 32 - x threads again await the result of this operation. 




Therefore, for buckets with a lot of free slots where insertions fail the majority of threads in the warp are inactive. Furthermore, the vector size generally does not exceed four integers, which means that when attempting to atomically insert a vector, the majority of threads in a warp is inactive. Ergo, one possible improvement over the hash table is to reduce the bucket size, so that there are fewer slots per bucket, and therefore, fewer threads are needed to insert a single vector. As a single insertion still uses one thread per integer in the bucket, in turn more vectors can be inserted in parallel.

Figure <ref> shows what the division of threads over a warp looks like if buckets of size 8 instead of 32 are used. As can be observed, a warp can insert four elements in parallel, as in this diagram each group of 8 threads inserts a different vector and accesses different buckets in global memory. 

The logical consequence of this improvement is that after scanning a bucket, fewer threads are inactive while the vector is being inserted into a free slot. If we suppose that the vector size for a certain model is 3, and that the new bucket size is 8, then while inserting a vector using the hash table 32 - 3 = 29 threads are inactive. On the other hand, if four buckets of size 8 are simultaneously accessed, only 32 - 3· 4 = 20 threads are inactive, and four vectors are simultaneously processed, as opposed to only one.

However, while more threads can be active at the same time, smaller buckets also lead to thread divergence within a warp. First of all, of course, accessing different buckets simultaneously likely leads to uncoalesced memory accesses. Furthermore, it is also possible that in an insertion procedure, one group needs to do more work than another in the same warp. For instance, consider that the first group in the warp fails to find its vector in the designated bucket, and also cannot write it to the bucket since the latter is full. In that case, the group needs to fetch another bucket. At the same time, another group in the warp may find its vector in the designated bucket, and therefore be able to stop the insertion procedure. In such a situation, these two groups will diverge, and the second group will have to wait until the first group has finished inserting. This means that the use of smaller buckets can only be advantageous if the performance increase of the smaller buckets outweighs the performance penalty of divergence. In this paper, we address whether this is true or not in practical explorations. 

The suggested performance increase has been experimentally validated by comparing an implementation of the hash table with varying bucket size to the original 2.0 hash table, both in isolation and as a part of . The results of this comparison are presented and discussed in section <ref>.



§ EXPERIMENTS


In this section we discuss our experiments to evaluate the scalability of . In Section <ref>, we report on experiments to evaluate the effect of the bucket size on the runtimes of . In the next two sections, our goal is to determine whether scales well when varying the size of the model and the performance of the hardware, respectively. For most of the experiments, we use an NVIDIA Titan X (Maxwell) installed on a machine running Linux Mint 17.2. The number of blocks is set to 6,144, with 512 threads per block. The hash table is allocated 5GB in global memory. 

For our benchmarks, we selected a varied set of models from the CADP toolset <cit.>, the mCRL2 toolset <cit.> and the BEEM database <cit.>. The models with a .1 suffix have been modified to obtain a larger state space.



 §.§ Varying the Bucket Size



To test different bucket sizes two types of experiments have been performed. First, the hash table with varying bucket sizes has been tested in isolation, where the time taken to insert 100,000,000 elements has been measured. Second, has been modified such that it uses a hash table with modifiable bucket size. This bucket size can be set at compile time. The performance of this version of has been compared to the original w.r.t. the exploration of several input models. 




The input data for the performance evaluation of the hash tables in isolation is a set of sequences of randomly generated integers, each sequence consisting of 100,000,000 vectors with a length of 1 integer. The sequences vary in how often an element occurs in the sequence. A duplication of 1 means that every unique element occurs once in the sequence, and a duplication of 100 means that each unique element in the sequence occurs 100 times. Therefore a sequence with a duplication of 100 has 100,000,000/100 unique elements. Note that this experiment tries to replicate state space exploration, where many duplicate insertions are performed when the fan-in is high, i.e., many transitions in the state space lead to the same state. So this experiment replicates the performance of the hash table for models that vary in the average fan-in of the states. For each sequence, we measured the total time required to insert all elements. 

The results of this comparison are depicted in Figure <ref>. We refer to the standard 2.0 hash table as and the hash table with configurable bucket size as . In the experiment, with a bucket size of four integers has been compared to . is slower for sequences where each element only occurs a few times. However, for sequences with a higher duplication degree starts to outperform . After all, for the sequences with more duplication, less time is spent on atomically inserting elements, since most elements are already in the hash table. performs up to three times better than . We will see later why the amount of duplication is relevant for model checking. 


In addition to testing the performance of the hash tables in isolation, the performance of has been compared with standard hashing as a part of 2.0. The hash table underlying , as implemented by Wijs, Neele and  <cit.>, has been modified to allow configuration of the bucket size at compile time. We compared the time required for state space exploration of our implementation, with configurable bucket size, with the original implementation of 2.0 <cit.>. 

Four bucket sizes have been compared to 2.0, namely bucket sizes 4, 8, 16 and 32 integers. The relative performance with these bucket sizes has been recorded with the performance of 2.0 as baseline. For each model the experiment has been run five times and the average running time of these five runs has been taken. 




The result of these comparisons is illustrated in Figure <ref>. As can be observed the total exploration time of with configurable bucket size is for most models larger than the runtime of 2.0. Only szymanski5 and lann7 show a small performance increase for bucket size 4. For the other instances, however, the new hash table causes a slow down of up to 30%.

There are three reasons why the promising performance shown in the previous experiments is not reflected here. First, the increased complexity of the hash table negatively affects register pressure, i.e., the number of registers each thread requires to execute the kernel. When the register usage in a kernel increases, the compiler may temporarily place register contents in global memory. This effect is not observed when the hash table is tested in isolation, as in that case, far fewer registers per thread are required. The increase in register pressure is also the reason that with a configurable bucket size set to 32 is slower than with a static bucket size of 32.

Furthermore, smaller bucket sizes result in more thread divergence and more uncoalesced memory accesses when reading from the hash table. Therefore, the available memory bandwidth is used less efficiently, leading to a drop in performance. Apparently, the increased potential for parallel insertions of vectors cannot overcome this drawback.

Lastly, while exploring the state-space, only discovers duplicates if those states have several incoming transitions. On average, the models used for the experiments have a fan-in of 4 to 6, with some exceptions that have a higher fan-in of around 8 to 11. However, from Figure <ref> it can be concluded that the hash table in isolation only starts to outperform the static hash table when each element is duplicated 21 times. This partly explains the performance seen in Figure <ref>. 




 §.§ Varying the Model Size

In addition to experimentally comparing the effect of different bucket sizes, we also investigated how 2.0 behaves when exploring state spaces of different size. We performed this experiment with two different models. The first is a version of the Gas Station model <cit.> where the number of pumps is fixed to two. We varied the number of customers between two and twelve. None of these instances requires a state vector longer than two 32-bit integers.

The other model is a simple implementation of a ring-structured network, where one token is continuously passed forward between the nodes. Each node has two transitions to communicate with its neighbours and a further three internal transitions. Here, we varied the number of nodes in the network.



We executed the tool five times on each instance and computed the average runtime. The results are listed in Table <ref>. For the smallest instances, the performance of (measured in states/sec) is a lot worse compared to the larger instances. This has two main reasons. First of all, the relative overhead suffered from initialization and work scanning is higher. Second, the parallelism offered by the GPU cannot be fully exploited, because the size of one search layer is too small to occupy all blocks with work.

For the gas station model, peak performance is achieved for the instance with ten customers, which has 60 million states. For larger instances, the performance decreases slightly due to the increasing occupancy of the hash table. This leads to more hash collisions, therefore more time is lost on rehashing.

The results of the token ring model show another interesting scalability aspect. There is a performance drop between the instances with 10 nodes and 11 nodes. This is caused by the fact that the instance with 11 nodes is the smallest for which the state vector exceeds 32 bits in length. Longer state vectors lead to more memory accesses throughout the state-space generation algorithm.




 §.§ Speed-up Due to the Pascal Architecture

The Titan X we used for most of the benchmarks is based on the Maxwell architecture, and was launched in 2015. Since then, NVIDIA has released several other high-end GPUs. Most aspects have been improved: the architecture has been revised, there are more CUDA cores on the GPU and there is more global memory available. To investigate how well scales with faster hardware, we performed several experiments with a Titan X with the Maxwell architecture (TXM) and with a Titan X with the Pascal architecture (TXP). The latter was released in 2016, and the one we used is installed in the Das-5 cluster <cit.> on a node running CentOS Linux 7.2.




The TXM experiments were performed with 6,144 blocks, while for the TXP, was set to use 14,336 blocks. The improvements of the hardware allows for to launch more blocks. To evaluate the speed-ups compared to a single-core CPU approach, we also conducted experiments with the Generator tool of the latest version (2017-i) of the Cadp toolbox <cit.>. These have been performed on nodes of the Das-5 cluster, which are equipped with an Intel Haswell E5-2630-v3 2.4 GHz CPU, 64 GB memory, and CentOS Linux 7.2.

The results are listed in Table <ref>. The reported runtimes are averages after having run the corresponding tool ten times. For most of the larger models, we see a speed-up of about 2 times when running on a TXP compared to running it on a TXM. The average speed-up is 1.73. This indicates that scales well with a higher memory bandwidth and a larger amount of CUDA cores.



Comparing on a TXP with single-core CPU analysis, the average speed-up is 183.91, and if we only take state spaces into account consisting of at least 10 million states, the average speed-up is 280.81.
Considering that with multi-core model checking, linear speed-ups can be achieved <cit.>, this roughly amounts to using 180 and 280 CPU cores, respectively. This, in combination with the observation that frequently, speed-ups over 300 times and once even over 500 times are achieved, clearly demonstrates the effectiveness of using GPUs for explicit-state model checking.





§ CONCLUSION AND FUTURE WORK



In this paper, we have reported on a number of scalability experiments we conducted with the GPU explicit-state model checker . In earlier work, we identified potential to further improve its hash table <cit.>. However, experiments in which we varied the bucket size in provided the insight that only for very specific input models, and only if the bucket size is set very small (4), some speed-up becomes noticeable. In the context of the entire computation of , the additional register use per thread and the introduced uncoalesced memory accesses and thread divergence make it not worthwile to make the bucket size configurable. This may be different for other applications, as our experiments with the hash table in isolation point out that hashing can be made more efficient in this way.

Besides this, we have also conducted experiments with models of different sizes. We scaled up a Gas Station model and a Token Ring model and obtained very encouraging results; for the second model, can generate up to 2.1 million states per second, and for the first model, at its peak, is able to generate about 5.5 million states per second, exploring a state space of 934.5 million states in under three minutes. We believe these are very impressive numbers that demonstrate the potential of GPU model checking.

Finally, we reported on some experiments we conducted with new GPU hardware. The Titan X with the Pascal architecture from 2016 provides for our benchmark set of models an average speed-up of 1.73 w.r.t. the Titan X with the Maxwell architecture from 2015. We also compared the runtimes of running on the Pascal Titan X with the CPU single-core Generator tool of the Cadp toolbox, and measured an average speed-up of 183.91 for the entire benchmark set of models, and of 280.81 for the models yielding a state space of at least 10 million states.
Often speed-ups over 300 times have been observed, and in one case even over 500 times.



  
Future work
For future work, we will consider various possible extensions to the tool. First of all, the ability to write
explored state spaces to disk will open up the possibility to postprocess and further analyse the state spaces. This could be done directly, or after application of bisimulation reduction on the GPU <cit.>.

Second of all, we will work on making the tool more user friendly. Currently, providing an input model is quite cumbersome, since requires a user to express system behaviour in the low level description formalism of networks of LTSs. Specifying systems would be much more convenient if a higher-level modelling language would be supported. We will investigate which modelling languages would be most suitable for integration in the current tool.

Finally, we will also consider the application of to conduct computations similar to model checking, such as performance analysis <cit.>. This requires to incorporate time into the input models, for instance by including special actions to represent the passage of time <cit.>.

eptcs

