diff --git a/spot/mc/Makefile.am b/spot/mc/Makefile.am index b5d963cb9..e08460207 100644 --- a/spot/mc/Makefile.am +++ b/spot/mc/Makefile.am @@ -22,7 +22,7 @@ AM_CXXFLAGS = $(WARNING_CXXFLAGS) mcdir = $(pkgincludedir)/mc mc_HEADERS = reachability.hh intersect.hh ec.hh unionfind.hh utils.hh\ - mc.hh deadlock.hh bloemen.hh + mc.hh deadlock.hh bloemen.hh cndfs.hh noinst_LTLIBRARIES = libmc.la diff --git a/spot/mc/cndfs.hh b/spot/mc/cndfs.hh new file mode 100644 index 000000000..14d5271bd --- /dev/null +++ b/spot/mc/cndfs.hh @@ -0,0 +1,546 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015, 2016, 2017, 2018, 2019 Laboratoire de Recherche et +// Developpement de l'Epita +// +// This file is part of Spot, a model checking library. +// +// Spot is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// Spot is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace spot +{ + /// \brief This object is returned by the algorithm below + struct SPOT_API cndfs_stats + { + unsigned states; ///< \brief Number of states visited + unsigned transitions; ///< \brief Number of transitions visited + unsigned instack_dfs; ///< \brief Maximum DFS stack + bool is_empty; ///< \brief Is the model empty + unsigned walltime; ///< \brief Walltime for this thread in ms + }; + + template + class swarmed_cndfs + { + struct local_colors + { + bool cyan; + bool is_in_Rp; + }; + + /// \brief The colors of a state + struct cndfs_colors + { + std::atomic blue; + std::atomic red; + local_colors l[1]; + }; + + struct product_state + { + State st_kripke; + unsigned st_prop; + cndfs_colors* colors; + }; + + /// \brief The hasher for the previous state. + struct state_hasher + { + state_hasher(const product_state&) + { } + + state_hasher() = default; + + brick::hash::hash128_t + hash(const product_state& lhs) const + { + StateHash hash; + // Not modulo 31 according to brick::hashset specifications. + unsigned u = hash(lhs.st_kripke) % (1<<30); + u = wang32_hash(lhs.st_prop) ^ u; + u = u % (1<<30); + return {u, u}; + } + + bool equal(const product_state& lhs, + const product_state& rhs) const + { + StateEqual equal; + return (lhs.st_prop == rhs.st_prop) + && equal(lhs.st_kripke, rhs.st_kripke); + } + }; + + struct todo__element + { + product_state st; + SuccIterator* it_kripke; + std::shared_ptr it_prop; + bool from_accepting; + }; + + public: + + ///< \brief Shortcut to ease shared map manipulation + using shared_map = brick::hashset::FastConcurrent ; + + swarmed_cndfs(kripkecube& sys, twacube_ptr twa, + shared_map& map, unsigned tid, std::atomic& stop): + sys_(sys), twa_(twa), tid_(tid), map_(map), + nb_th_(std::thread::hardware_concurrency()), + p_colors_(sizeof(cndfs_colors) + + sizeof(local_colors)*(std::thread::hardware_concurrency() - 1)), + stop_(stop) + { + SPOT_ASSERT(is_a_kripkecube(sys)); + } + + virtual ~swarmed_cndfs() + { + while (!todo_blue_.empty()) + { + sys_.recycle(todo_blue_.back().it_kripke, tid_); + todo_blue_.pop_back(); + } + while (!todo_red_.empty()) + { + sys_.recycle(todo_red_.back().it_kripke, tid_); + todo_red_.pop_back(); + } + } + + void setup() + { + tm_.start("DFS thread " + std::to_string(tid_)); + } + + std::pair + push_blue(product_state s, bool from_accepting) + { + cndfs_colors* c = (cndfs_colors*) p_colors_.allocate(); + c->red = false; + c->blue = false; + for (unsigned i = 0; i < nb_th_; ++i) + { + c->l[i].cyan = false; + c->l[i].is_in_Rp = false; + } + + s.colors = c; + + // Try to insert the new state in the shared map. + auto it = map_.insert(s); + bool b = it.isnew(); + + // Insertion failed, delete element + // FIXME Should we add a local cache to avoid useless allocations? + if (!b) + { + p_colors_.deallocate(c); + bool blue = ((*it)).colors->blue.load(); + bool cyan = ((*it)).colors->l[tid_].cyan; + if (blue || cyan) + return {false, *it}; + } + + // Mark state as visited. + ((*it)).colors->l[tid_].cyan = true; + ++states_; + todo_blue_.push_back({*it, + sys_.succ(((*it)).st_kripke, tid_), + twa_->succ(((*it)).st_prop), + from_accepting}); + return {true, *it}; + } + + bool pop_blue() + { + // Track maximum dfs size + dfs_ = todo_blue_.size() > dfs_ ? todo_blue_.size() : dfs_; + + todo_blue_.back().st.colors->l[tid_].cyan = false; + sys_.recycle(todo_blue_.back().it_kripke, tid_); + todo_blue_.pop_back(); + return true; + } + + std::pair + push_red(product_state s, bool ignore_cyan) + { + // Try to insert the new state in the shared map. + auto it = map_.insert(s); + bool b = it.isnew(); + + SPOT_ASSERT(!b); // should never be new in a red DFS + bool red = ((*it)).colors->red.load(); + bool cyan = ((*it)).colors->l[tid_].cyan; + bool in_Rp = ((*it)).colors->l[tid_].is_in_Rp; + if (red || (cyan && !ignore_cyan) || in_Rp) + return {false, *it}; // couldn't insert + + // Mark state as visited. + ((*it)).colors->l[tid_].is_in_Rp = true; + Rp_.push_back(*it); + ++states_; + todo_red_.push_back({*it, + sys_.succ(((*it)).st_kripke, tid_), + twa_->succ(((*it)).st_prop), + false}); + return {true, *it}; + } + + bool pop_red() + { + // Track maximum dfs size + dfs_ = todo_blue_.size() + todo_red_.size() > dfs_ ? + todo_blue_.size() + todo_red_.size() : dfs_; + + + sys_.recycle(todo_red_.back().it_kripke, tid_); + todo_red_.pop_back(); + return true; + } + + void finalize() + { + tm_.stop("DFS thread " + std::to_string(tid_)); + } + + unsigned states() + { + return states_; + } + + unsigned transitions() + { + return transitions_; + } + + void run() + { + setup(); + blue_dfs(); + finalize(); + } + + void blue_dfs() + { + product_state initial = {sys_.initial(tid_), + twa_->get_initial(), + nullptr}; + if (!push_blue(initial, false).first) + return; + + // Property automaton has only one state + if (todo_blue_.back().it_prop->done()) + return; + + forward_iterators(todo_blue_, true); + + while (!todo_blue_.empty() && !stop_.load(std::memory_order_relaxed)) + { + auto current = todo_blue_.back(); + + if (!current.it_kripke->done()) + { + ++transitions_; + product_state s = { + current.it_kripke->state(), + twa_->trans_storage(current.it_prop, tid_).dst, + nullptr + }; + + bool acc = (bool) twa_->trans_storage(current.it_prop, tid_).acc_; + forward_iterators(todo_blue_, false); + + auto tmp = push_blue(s, acc); + if (tmp.first) + forward_iterators(todo_blue_, true); + else if (acc) + { + // The state cyan and we can reach it throught an + // accepting transition, a accepting cycle has been + // found without launching a red dfs + if (tmp.second.colors->l[tid_].cyan) + { + cycle_start_ = s; + is_empty_ = false; + stop_.store(true); + return; + } + + SPOT_ASSERT(tmp.second.colors->blue); + + red_dfs(s); + if (!is_empty_) + return; + post_red_dfs(); + } + } + else + { + current.st.colors->blue.store(true); + + // backtracked an accepting transition; launch red DFS + if (current.from_accepting) + { + red_dfs(todo_blue_.back().st); + if (!is_empty_) + return; + post_red_dfs(); + } + + pop_blue(); + } + } + } + + void post_red_dfs() + { + for (product_state& s : Rp_acc_) + { + while (s.colors->red.load() && !stop_.load()) + { + // await + } + } + for (product_state& s : Rp_) + { + s.colors->red.store(true); + s.colors->l[tid_].is_in_Rp = false; // empty Rp + } + + Rp_.clear(); + Rp_acc_.clear(); + } + + void red_dfs(product_state initial) + { + auto init_push = push_red(initial, true); + SPOT_ASSERT(init_push.second.colors->blue); + + if (!init_push.first) + return; + + forward_iterators(todo_red_, true); + + while (!todo_red_.empty() && !stop_.load(std::memory_order_relaxed)) + { + auto current = todo_red_.back(); + + if (!current.it_kripke->done()) + { + ++transitions_; + product_state s = { + current.it_kripke->state(), + twa_->trans_storage(current.it_prop, tid_).dst, + nullptr + }; + bool acc = (bool) twa_->trans_storage(current.it_prop, tid_).acc_; + forward_iterators(todo_red_, false); + + auto res = push_red(s, false); + if (res.first) // could push properly + { + forward_iterators(todo_red_, true); + + SPOT_ASSERT(res.second.colors->blue); + + // The transition is accepting, we want to keep + // track of this state + if (acc) + { + // Do not insert twice a state + bool found = false; + for (auto& st: Rp_acc_) + { + if (st.colors == res.second.colors) + { + found = true; + break; + } + } + if (!found) + Rp_acc_.push_back(Rp_.back()); + } + } + else + { + if (res.second.colors->l[tid_].cyan) + { + // color pointers are unique to each element, + // comparing them is equivalent (but faster) to comparing + // st_kripke and st_prop individually. + if (init_push.second.colors == res.second.colors && !acc) + continue; + is_empty_ = false; + cycle_start_ = s; + stop_.store(true); + return; + } + else if (acc && res.second.colors->l[tid_].is_in_Rp) + { + auto it = map_.insert(s); + Rp_acc_.push_back(*it); + } + } + } + else + { + pop_red(); + } + } + } + + std::string trace() + { + SPOT_ASSERT(!is_empty()); + StateEqual equal; + auto state_equal = [equal](product_state a, product_state b) + { + return a.st_prop == b.st_prop + && equal(a.st_kripke, b.st_kripke); + }; + + std::string res = "Prefix:\n"; + + auto it = todo_blue_.begin(); + while (it != todo_blue_.end()) + { + if (state_equal(((*it)).st, cycle_start_)) + break; + res += " " + std::to_string(((*it)).st.st_prop) + + "*" + sys_.to_string(((*it)).st.st_kripke) + "\n"; + ++it; + } + + res += "Cycle:\n"; + while (it != todo_blue_.end()) + { + res += " " + std::to_string(((*it)).st.st_prop) + + "*" + sys_.to_string(((*it)).st.st_kripke) + "\n"; + ++it; + } + + if (!todo_red_.empty()) + { + it = todo_red_.begin() + 1; // skip first element, also in blue + while (it != todo_red_.end()) + { + res += " " + std::to_string(((*it)).st.st_prop) + + "*" + sys_.to_string(((*it)).st.st_kripke) + "\n"; + ++it; + } + } + res += " " + std::to_string(cycle_start_.st_prop) + + "*" + sys_.to_string(cycle_start_.st_kripke) + "\n"; + + return res; + } + + bool is_empty() + { + return is_empty_; + } + + unsigned walltime() + { + return tm_.timer("DFS thread " + std::to_string(tid_)).walltime(); + } + + cndfs_stats stats() + { + return {states(), transitions(), dfs_, is_empty(), walltime()}; + } + + protected: + void forward_iterators(std::vector& todo, bool just_pushed) + { + SPOT_ASSERT(!todo.empty()); + + auto top = todo.back(); + + SPOT_ASSERT(!(top.it_prop->done() && + top.it_kripke->done())); + + // Sometimes kripke state may have no successors. + if (top.it_kripke->done()) + return; + + // The state has just been push and the 2 iterators intersect. + // There is no need to move iterators forward. + SPOT_ASSERT(!(top.it_prop->done())); + if (just_pushed && twa_->get_cubeset() + .intersect(twa_->trans_data(top.it_prop, tid_).cube_, + top.it_kripke->condition())) + return; + + // Otherwise we have to compute the next valid successor (if it exits). + // This requires two loops. The most inner one is for the twacube since + // its costless + if (top.it_prop->done()) + top.it_prop->reset(); + else + top.it_prop->next(); + + while (!top.it_kripke->done()) + { + while (!top.it_prop->done()) + { + if (twa_->get_cubeset() + .intersect(twa_->trans_data(top.it_prop, tid_).cube_, + top.it_kripke->condition())) + return; + top.it_prop->next(); + } + top.it_prop->reset(); + top.it_kripke->next(); + } + } + + private: + kripkecube& sys_; + twacube_ptr twa_; + std::vector todo_blue_; + std::vector todo_red_; + unsigned transitions_ = 0; ///< \brief Number of transitions + unsigned tid_; ///< \brief Thread's current ID + shared_map map_; ///< \brief Map shared by threads + spot::timer_map tm_; ///< \brief Time execution + unsigned states_ = 0; ///< \brief Number of states + unsigned dfs_ = 0; ///< \brief Maximum DFS stack size + /// \brief Maximum number of threads that can be handled by this algorithm + unsigned nb_th_ = 0; + fixed_size_pool p_colors_; + bool is_empty_ = true; ///< \brief Accepting cycle detected? + std::atomic& stop_; ///< \brief Stop-the-world boolean + std::vector Rp_; + std::vector Rp_acc_; + product_state cycle_start_; + }; +} diff --git a/spot/mc/mc.hh b/spot/mc/mc.hh index 4140b9ec2..14a2c34b7 100644 --- a/spot/mc/mc.hh +++ b/spot/mc/mc.hh @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -327,4 +328,89 @@ namespace spot } return std::make_tuple(is_empty, trace, stats, tm); } + + /// \brief CNDFS + template + static std::tuple, + spot::timer_map> + cndfs(kripke_ptr sys, twacube_ptr prop, bool compute_ctrx = false) + { + spot::timer_map tm; + using algo_name = spot::swarmed_cndfs; + + unsigned nbth = sys->get_threads(); + typename algo_name::shared_map map; + std::atomic stop(false); + + tm.start("Initialisation"); + std::vector swarmed(nbth); + for (unsigned i = 0; i < nbth; ++i) + swarmed[i] = new algo_name(*sys, prop, map, i, stop); + tm.stop("Initialisation"); + + std::mutex iomutex; + std::atomic barrier(true); + std::vector threads(nbth); + for (unsigned i = 0; i < nbth; ++i) + { + threads[i] = std::thread ([&swarmed, &iomutex, i, & barrier] + { +#if defined(unix) || defined(__unix__) || defined(__unix) + { + std::lock_guard iolock(iomutex); + std::cout << "Thread #" << i + << ": on CPU " << sched_getcpu() << '\n'; + } +#endif + + // Wait all threads to be instanciated. + while (barrier) + continue; + swarmed[i]->run(); + }); + +#if defined(unix) || defined(__unix__) || defined(__unix) + // Pins threads to a dedicated core. + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(i, &cpuset); + int rc = pthread_setaffinity_np(threads[i].native_handle(), + sizeof(cpu_set_t), &cpuset); + if (rc != 0) + { + std::lock_guard iolock(iomutex); + std::cerr << "Error calling pthread_setaffinity_np: " << rc << '\n'; + } +#endif + } + + tm.start("Run"); + barrier.store(false); + + for (auto& t : threads) + t.join(); + tm.stop("Run"); + + std::string trace; + std::vector stats; + bool is_empty = true; + for (unsigned i = 0; i < sys->get_threads(); ++i) + { + if (!swarmed[i]->is_empty()) + { + is_empty = false; + if (compute_ctrx) + trace = swarmed[i]->trace(); + } + + stats.push_back(swarmed[i]->stats()); + } + for (unsigned i = 0; i < nbth; ++i) + delete swarmed[i]; + + return std::make_tuple(is_empty, trace, stats, tm); + } } diff --git a/tests/ltsmin/check.test b/tests/ltsmin/check.test index 659799fae..0a136b132 100755 --- a/tests/ltsmin/check.test +++ b/tests/ltsmin/check.test @@ -1,7 +1,7 @@ #!/bin/sh # -*- coding: utf-8 -*- -# Copyright (C) 2011, 2012, 2014, 2015, 2016, 2017, 2019 Laboratoire de Recherche -# et Développement de l'Epita (LRDE). +# Copyright (C) 2011, 2012, 2014, 2015, 2016, 2017, 2019 Laboratoire +# de Recherche et Développement de l'Epita (LRDE). # # This file is part of Spot, a model checking library. # @@ -93,3 +93,7 @@ test `grep "#" stdout | awk -F',' '{print $7}'` -eq 29115 run 0 ../modelcheck --model beem-peterson.4.dve2C \ --formula '!GF(P_0.CS|P_1.CS|P_2.CS|P_3.CS)' --csv --bloemen-ec -p 3 >stdout + +# Test CNDFS +run 0 ../modelcheck --model beem-peterson.4.dve2C \ + --formula '!GF(P_0.CS|P_1.CS|P_2.CS|P_3.CS)' --csv --cndfs -p 3 >stdout diff --git a/tests/ltsmin/modelcheck.cc b/tests/ltsmin/modelcheck.cc index d283dc3c7..8cf89b481 100644 --- a/tests/ltsmin/modelcheck.cc +++ b/tests/ltsmin/modelcheck.cc @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,7 @@ struct mc_options_ bool has_deadlock = false; bool bloemen = false; bool bloemen_ec = false; + bool cndfs = false; } mc_options; @@ -96,6 +98,9 @@ parse_opt_finput(int key, char* arg, struct argp_state*) case 'c': mc_options.compute_counterexample = true; break; + case 'C': + mc_options.cndfs = true; + break; case 'd': if (strcmp(arg, "model") == 0) mc_options.dot_output |= DOT_MODEL; @@ -158,6 +163,8 @@ static const argp_option options[] = "run the SCC computation of Bloemen et al. (PPOPP'16) with EC", 0}, { "bloemen", 'b', nullptr, 0, "run the SCC computation of Bloemen et al. (PPOPP'16)", 0 }, + { "cndfs", 'C', nullptr, 0, + "run CNDFS", 0 }, { "counterexample", 'c', nullptr, 0, "compute an accepting counterexample (if it exists)", 0 }, { "is-empty", 'e', nullptr, 0, @@ -304,7 +311,8 @@ static int checked_main() if (mc_options.nb_threads == 1 && mc_options.formula != nullptr && mc_options.model != nullptr && - !mc_options.bloemen_ec) + !mc_options.bloemen_ec && + !mc_options.cndfs) { product = spot::otf_product(model, prop); @@ -417,7 +425,8 @@ static int checked_main() if (mc_options.nb_threads != 1 && mc_options.formula != nullptr && mc_options.model != nullptr && - !mc_options.bloemen_ec) + !mc_options.bloemen_ec && + !mc_options.cndfs) { unsigned int hc = std::thread::hardware_concurrency(); if (mc_options.nb_threads > hc) @@ -623,6 +632,117 @@ static int checked_main() } } + if (mc_options.cndfs && + mc_options.model != nullptr && mc_options.formula != nullptr) + { + unsigned int hc = std::thread::hardware_concurrency(); + if (mc_options.nb_threads > hc) + std::cerr << "Warning: you require " << mc_options.nb_threads + << " threads, but your computer only support " << hc + << ". This could slow down parallel algorithms.\n"; + + // Only support Single Acceptance Conditions + tm.start("degeneralize"); + auto prop_degen = spot::degeneralize_tba(prop); + tm.stop("degeneralize"); + + tm.start("twa to twacube"); + auto propcube = spot::twa_to_twacube(prop_degen); + tm.stop("twa to twacube"); + + tm.start("load kripkecube"); + spot::ltsmin_kripkecube_ptr modelcube = nullptr; + try + { + modelcube = spot::ltsmin_model::load(mc_options.model) + .kripkecube(propcube->get_ap(), deadf, mc_options.compress, + mc_options.nb_threads); + } + catch (const std::runtime_error& e) + { + std::cerr << e.what() << '\n'; + } + tm.stop("load kripkecube"); + + int memused = spot::memusage(); + tm.start("cndfs"); + auto res = spot::cndfs + (modelcube, propcube, mc_options.compute_counterexample); + tm.stop("cndfs"); + memused = spot::memusage() - memused; + + if (!modelcube) + { + exit_code = 2; + goto safe_exit; + } + + // Display statistics + unsigned smallest = 0; + for (unsigned i = 0; i < std::get<2>(res).size(); ++i) + { + if (std::get<2>(res)[i].states < std::get<2>(res)[smallest].states) + smallest = i; + + std::cout << "\n---- Thread number : " << i << '\n'; + std::cout << std::get<2>(res)[i].states << " unique states visited\n"; + std::cout << std::get<2>(res)[i].transitions + << " transitions explored\n"; + std::cout << std::get<2>(res)[i].instack_dfs + << " items max in DFS search stack\n"; + std::cout << std::get<2>(res)[i].walltime + << " milliseconds\n"; + + if (mc_options.csv) + { + std::cout << "Find following the csv: " + << "thread_id,walltimems,type," + << "states,transitions\n"; + std::cout << "@th_" << i << ',' + << std::get<2>(res)[i].walltime << ',' + << (std::get<2>(res)[i].is_empty ? + "EMPTY," : "NONEMPTY,") + << std::get<2>(res)[i].states << ',' + << std::get<2>(res)[i].transitions + << std::endl; + } + } + + if (mc_options.csv) + { + std::cout << "\nSummary :\n"; + if (std::get<0>(res)) + std::cout << "no accepting run found\n"; + else if (!mc_options.compute_counterexample) + { + std::cout << "an accepting run exists " + << "(use -c to print it)" << std::endl; + exit_code = 1; + } + else + std::cout << "an accepting run exists\n" + << std::get<1>(res) << '\n'; + + std::cout << "Find following the csv: " + << "model,walltimems,memused,type," + << "states,transitions\n"; + + std::cout << '#' + << split_filename(mc_options.model) + << ',' + << tm.timer("cndfs").walltime() << ',' + << memused << ',' + << (std::get<0>(res) ? "EMPTY," : "NONEMPTY,") + << std::get<2>(res)[smallest].states << ',' + << std::get<2>(res)[smallest].transitions + << '\n'; + } + } + if (mc_options.bloemen && mc_options.model != nullptr) { unsigned int hc = std::thread::hardware_concurrency();