#include "storm/storage/dd/bisimulation/SignatureRefiner.h" #include #include #include "storm/storage/dd/DdManager.h" #include "storm/storage/dd/cudd/InternalCuddDdManager.h" #include "storm/utility/macros.h" #include "storm/exceptions/InvalidSettingsException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/settings/SettingsManager.h" #include "storm/settings/modules/BisimulationSettings.h" #include #include "sylvan_cache.h" #include "sylvan_table.h" #include "sylvan_int.h" namespace storm { namespace dd { namespace bisimulation { struct CuddPointerPairHash { std::size_t operator()(std::pair const& pair) const { std::size_t seed = std::hash()(pair.first); spp::hash_combine(seed, pair.second); return seed; } }; struct SylvanMTBDDPairHash { std::size_t operator()(std::pair const& pair) const { std::size_t seed = std::hash()(pair.first); spp::hash_combine(seed, pair.second); return seed; } }; struct SylvanMTBDDPairLess { std::size_t operator()(std::pair const& a, std::pair const& b) const { if (a.first < b.first) { return true; } else if (a.first == b.first && a.second < b.second) { return true; } return false; } }; template class InternalSignatureRefiner; class ReuseWrapper { public: ReuseWrapper() : ReuseWrapper(false) { // Intentionally left empty. } ReuseWrapper(bool value) : value(value) { // Intentionally left empty. } bool isReused() const { return value; } void setReused() { value = true; } private: bool value; }; template class InternalSignatureRefiner { public: InternalSignatureRefiner(storm::dd::DdManager const& manager, storm::expressions::Variable const& blockVariable, uint64_t lastStateLevel) : manager(manager), internalDdManager(manager.getInternalDdManager()), blockVariable(blockVariable), lastStateLevel(lastStateLevel), nextFreeBlockIndex(0), numberOfRefinements(0), lastNumberOfVisitedNodes(10000), signatureCache(lastNumberOfVisitedNodes), reuseBlocksCache(lastNumberOfVisitedNodes) { // Intentionally left empty. } Partition refine(Partition const& oldPartition, Signature const& signature) { storm::dd::Add newPartitionAdd = refine(oldPartition, signature.getSignatureAdd()); ++numberOfRefinements; return oldPartition.replacePartition(newPartitionAdd, nextFreeBlockIndex); } private: storm::dd::Add refine(Partition const& oldPartition, storm::dd::Add const& signatureAdd) { STORM_LOG_ASSERT(oldPartition.storedAsAdd(), "Expecting partition to be stored as ADD for CUDD."); // Clear the caches. signatureCache.clear(); reuseBlocksCache.clear(); nextFreeBlockIndex = oldPartition.getNextFreeBlockIndex(); // Perform the actual recursive refinement step. DdNodePtr result = refine(oldPartition.asAdd().getInternalAdd().getCuddDdNode(), signatureAdd.getInternalAdd().getCuddDdNode()); // Construct resulting ADD from the obtained node and the meta information. storm::dd::InternalAdd internalNewPartitionAdd(&internalDdManager, cudd::ADD(internalDdManager.getCuddManager(), result)); storm::dd::Add newPartitionAdd(oldPartition.asAdd().getDdManager(), internalNewPartitionAdd, oldPartition.asAdd().getContainedMetaVariables()); return newPartitionAdd; } DdNodePtr refine(DdNode* partitionNode, DdNode* signatureNode) { ::DdManager* ddman = internalDdManager.getCuddManager().getManager(); // If we arrived at the constant zero node, then this was an illegal state encoding (we require // all states to be non-deadlock). if (partitionNode == Cudd_ReadZero(ddman)) { return partitionNode; } // Check the cache whether we have seen the same node before. std::unique_ptr& sigCacheEntrySmartPtr = signatureCache[std::make_pair(signatureNode, partitionNode)]; if (sigCacheEntrySmartPtr) { // If so, we return the corresponding result. return *sigCacheEntrySmartPtr; } DdNode** newEntryPtr = new DdNode*; sigCacheEntrySmartPtr.reset(newEntryPtr); // Determine the levels in the DDs. uint64_t partitionVariable = Cudd_NodeReadIndex(partitionNode) - 1; uint64_t signatureVariable = Cudd_NodeReadIndex(signatureNode); uint64_t partitionLevel = Cudd_ReadPerm(ddman, partitionVariable); uint64_t signatureLevel = Cudd_ReadPerm(ddman, signatureVariable); uint64_t topLevel = std::min(partitionLevel, signatureLevel); uint64_t topVariable = topLevel == partitionLevel ? partitionVariable : signatureVariable; // Check whether the top variable is still within the state encoding. if (topLevel <= lastStateLevel) { // Determine subresults by recursive descent. DdNodePtr thenResult; DdNodePtr elseResult; if (partitionLevel < signatureLevel) { thenResult = refine(Cudd_T(partitionNode), signatureNode); Cudd_Ref(thenResult); elseResult = refine(Cudd_E(partitionNode), signatureNode); Cudd_Ref(elseResult); } else if (partitionLevel > signatureLevel) { thenResult = refine(partitionNode, Cudd_T(signatureNode)); Cudd_Ref(thenResult); elseResult = refine(partitionNode, Cudd_E(signatureNode)); Cudd_Ref(elseResult); } else { thenResult = refine(Cudd_T(partitionNode), Cudd_T(signatureNode)); Cudd_Ref(thenResult); elseResult = refine(Cudd_E(partitionNode), Cudd_E(signatureNode)); Cudd_Ref(elseResult); } DdNode* result; if (thenResult == elseResult) { Cudd_Deref(thenResult); Cudd_Deref(elseResult); result = thenResult; } else { // Get the node to connect the subresults. DdNode* var = Cudd_addIthVar(ddman, topVariable + 1); Cudd_Ref(var); result = Cudd_addIte(ddman, var, thenResult, elseResult); Cudd_Ref(result); Cudd_RecursiveDeref(ddman, var); Cudd_Deref(thenResult); Cudd_Deref(elseResult); } // Store the result in the cache. *newEntryPtr = result; Cudd_Deref(result); return result; } else { // If we are not within the state encoding any more, we hit the signature itself. // If this is the first time (in this traversal) that we encounter this signature, we check // whether we can assign the old block number to it. auto& reuseEntry = reuseBlocksCache[partitionNode]; if (!reuseEntry.isReused()) { reuseEntry.setReused(); *newEntryPtr = partitionNode; return partitionNode; } else { DdNode* result; { storm::dd::Add blockEncoding = manager.getEncoding(blockVariable, nextFreeBlockIndex, false).template toAdd(); ++nextFreeBlockIndex; result = blockEncoding.getInternalAdd().getCuddDdNode(); Cudd_Ref(result); } *newEntryPtr = result; Cudd_Deref(result); return result; } } } storm::dd::DdManager const& manager; storm::dd::InternalDdManager const& internalDdManager; storm::expressions::Variable const& blockVariable; // The last level that belongs to the state encoding in the DDs. uint64_t lastStateLevel; // The current number of blocks of the new partition. uint64_t nextFreeBlockIndex; // The number of completed refinements. uint64_t numberOfRefinements; // The number of nodes visited in the last refinement operation. uint64_t lastNumberOfVisitedNodes; // The cache used to identify states with identical signature. spp::sparse_hash_map, std::unique_ptr, CuddPointerPairHash> signatureCache; // The cache used to identify which old block numbers have already been reused. spp::sparse_hash_map reuseBlocksCache; }; template class InternalSignatureRefiner { public: InternalSignatureRefiner(storm::dd::DdManager const& manager, storm::expressions::Variable const& blockVariable, uint64_t lastStateLevel) : manager(manager), internalDdManager(manager.getInternalDdManager()), blockVariable(blockVariable), numberOfBlockVariables(manager.getMetaVariable(blockVariable).getNumberOfDdVariables()), blockCube(manager.getMetaVariable(blockVariable).getCube()), lastStateLevel(lastStateLevel), nextFreeBlockIndex(0), numberOfRefinements(0), signatureCache() { // Perform garbage collection to clean up stuff not needed anymore. LACE_ME; sylvan_gc(); } Partition refine(Partition const& oldPartition, Signature const& signature) { storm::dd::Bdd newPartitionBdd = refine(oldPartition, signature.getSignatureAdd()); return oldPartition.replacePartition(newPartitionBdd, nextFreeBlockIndex); } private: storm::dd::Bdd refine(Partition const& oldPartition, storm::dd::Add const& signatureAdd) { STORM_LOG_ASSERT(oldPartition.storedAsBdd(), "Expecting partition to be stored as BDD for Sylvan."); LACE_ME; // Set up next refinement. ++numberOfRefinements; // Clear the caches. std::size_t oldSize = signatureCache.size(); signatureCache.clear(); signatureCache.reserve(3 * oldSize); reuseBlocksCache.clear(); reuseBlocksCache.reserve(3 * oldPartition.getNumberOfBlocks()); nextFreeBlockIndex = oldPartition.getNextFreeBlockIndex(); // Clear performance counters. // signatureCacheLookups = 0; // signatureCacheHits = 0; // numberOfVisitedNodes = 0; // totalSignatureCacheLookupTime = std::chrono::high_resolution_clock::duration(0); // totalSignatureCacheStoreTime = std::chrono::high_resolution_clock::duration(0); // totalReuseBlocksLookupTime = std::chrono::high_resolution_clock::duration(0); // totalLevelLookupTime = std::chrono::high_resolution_clock::duration(0); // totalBlockEncodingTime = std::chrono::high_resolution_clock::duration(0); // totalMakeNodeTime = std::chrono::high_resolution_clock::duration(0); // Perform the actual recursive refinement step. BDD result = refine(oldPartition.asBdd().getInternalBdd().getSylvanBdd().GetBDD(), signatureAdd.getInternalAdd().getSylvanMtbdd().GetMTBDD()); // Construct resulting BDD from the obtained node and the meta information. storm::dd::InternalBdd internalNewPartitionBdd(&internalDdManager, sylvan::Bdd(result)); storm::dd::Bdd newPartitionBdd(oldPartition.asBdd().getDdManager(), internalNewPartitionBdd, oldPartition.asBdd().getContainedMetaVariables()); // // Display some statistics. // STORM_LOG_TRACE("Refinement visited " << numberOfVisitedNodes << " nodes."); // STORM_LOG_TRACE("Current #nodes in table: " << llmsset_count_marked(nodes) << " of " << llmsset_get_size(nodes) << ", cache: " << cache_getused() << " of " << cache_getsize() << "."); // STORM_LOG_TRACE("Signature cache hits: " << signatureCacheHits << ", misses: " << (signatureCacheLookups - signatureCacheHits) << "."); // STORM_LOG_TRACE("Signature cache lookup time: " << std::chrono::duration_cast(totalSignatureCacheLookupTime).count() << "ms"); // STORM_LOG_TRACE("Signature cache store time: " << std::chrono::duration_cast(totalSignatureCacheStoreTime).count() << "ms"); // STORM_LOG_TRACE("Signature cache total time: " << std::chrono::duration_cast(totalSignatureCacheStoreTime + totalSignatureCacheLookupTime).count() << "ms"); // STORM_LOG_TRACE("Reuse blocks lookup time: " << std::chrono::duration_cast(totalReuseBlocksLookupTime).count() << "ms"); // STORM_LOG_TRACE("Level lookup time: " << std::chrono::duration_cast(totalLevelLookupTime).count() << "ms"); // STORM_LOG_TRACE("Block encoding time: " << std::chrono::duration_cast(totalBlockEncodingTime).count() << "ms"); // STORM_LOG_TRACE("Make node time: " << std::chrono::duration_cast(totalMakeNodeTime).count() << "ms"); return newPartitionBdd; } BDD encodeBlock(uint64_t blockIndex) { std::vector e(numberOfBlockVariables); for (uint64_t i = 0; i < numberOfBlockVariables; ++i) { e[i] = blockIndex & 1 ? 1 : 0; blockIndex >>= 1; } return sylvan_cube(blockCube.getInternalBdd().getSylvanBdd().GetBDD(), e.data()); } BDD refine(BDD partitionNode, MTBDD signatureNode) { LACE_ME; // If we arrived at the constant zero node, then this was an illegal state encoding (we require // all states to be non-deadlock). if (partitionNode == sylvan_false) { return partitionNode; } STORM_LOG_ASSERT(partitionNode != mtbdd_false, "Expected non-false node."); // ++numberOfVisitedNodes; // Check the cache whether we have seen the same node before. // ++signatureCacheLookups; // auto start = std::chrono::high_resolution_clock::now(); std::unique_ptr& sigCacheEntrySmartPtr = signatureCache[std::make_pair(signatureNode, partitionNode)]; if (sigCacheEntrySmartPtr) { // ++signatureCacheHits; // If so, we return the corresponding result. // auto end = std::chrono::high_resolution_clock::now(); // totalSignatureCacheLookupTime += end - start; return *sigCacheEntrySmartPtr; } // auto end = std::chrono::high_resolution_clock::now(); // totalSignatureCacheLookupTime += end - start; MTBDD* newEntryPtr = new MTBDD; sigCacheEntrySmartPtr.reset(newEntryPtr); sylvan_gc_test(); // Determine levels in the DDs. // start = std::chrono::high_resolution_clock::now(); BDDVAR signatureVariable = sylvan_isconst(signatureNode) ? 0xffffffff : sylvan_var(signatureNode); BDDVAR partitionVariable = sylvan_var(partitionNode) - 1; BDDVAR topVariable = std::min(signatureVariable, partitionVariable); // end = std::chrono::high_resolution_clock::now(); // totalLevelLookupTime += end - start; // Check whether the top variable is still within the state encoding. if (topVariable <= lastStateLevel) { // Determine subresults by recursive descent. BDD thenResult; BDD elseResult; if (partitionVariable < signatureVariable) { elseResult = refine(sylvan_low(partitionNode), signatureNode); thenResult = refine(sylvan_high(partitionNode), signatureNode); } else if (partitionVariable > signatureVariable) { elseResult = refine(partitionNode, sylvan_low(signatureNode)); thenResult = refine(partitionNode, sylvan_high(signatureNode)); } else { elseResult = refine(sylvan_low(partitionNode), sylvan_low(signatureNode)); thenResult = refine(sylvan_high(partitionNode), sylvan_high(signatureNode)); } BDD result; if (thenResult == elseResult) { result = thenResult; } else { // Get the node to connect the subresults. // start = std::chrono::high_resolution_clock::now(); result = sylvan_makenode(topVariable + 1, elseResult, thenResult); // end = std::chrono::high_resolution_clock::now(); // totalMakeNodeTime += end - start; } // Store the result in the cache. // start = std::chrono::high_resolution_clock::now(); *newEntryPtr = result; // end = std::chrono::high_resolution_clock::now(); // totalSignatureCacheStoreTime += end - start; return result; } else { // If we are not within the state encoding any more, we hit the signature itself. // If this is the first time (in this traversal) that we encounter this signature, we check // whether we can assign the old block number to it. // start = std::chrono::high_resolution_clock::now(); auto& reuseBlockEntry = reuseBlocksCache[partitionNode]; // end = std::chrono::high_resolution_clock::now(); // totalReuseBlocksLookupTime += end - start; if (!reuseBlockEntry.isReused()) { reuseBlockEntry.setReused(); reuseBlocksCache.emplace(partitionNode, true); // start = std::chrono::high_resolution_clock::now(); *newEntryPtr = partitionNode; // end = std::chrono::high_resolution_clock::now(); // totalSignatureCacheStoreTime += end - start; return partitionNode; } else { // start = std::chrono::high_resolution_clock::now(); BDD result = encodeBlock(nextFreeBlockIndex++); // end = std::chrono::high_resolution_clock::now(); // totalBlockEncodingTime += end - start; // start = std::chrono::high_resolution_clock::now(); *newEntryPtr = result; // end = std::chrono::high_resolution_clock::now(); // totalSignatureCacheStoreTime += end - start; return result; } } } storm::dd::DdManager const& manager; storm::dd::InternalDdManager const& internalDdManager; storm::expressions::Variable const& blockVariable; uint64_t numberOfBlockVariables; storm::dd::Bdd blockCube; // The last level that belongs to the state encoding in the DDs. uint64_t lastStateLevel; // The current number of blocks of the new partition. uint64_t nextFreeBlockIndex; // The number of completed refinements. uint64_t numberOfRefinements; // The cache used to identify states with identical signature. spp::sparse_hash_map, std::unique_ptr, SylvanMTBDDPairHash> signatureCache; // The cache used to identify which old block numbers have already been reused. spp::sparse_hash_map reuseBlocksCache; // Performance counters. // uint64_t signatureCacheLookups; // uint64_t signatureCacheHits; // uint64_t numberOfVisitedNodes; // std::chrono::high_resolution_clock::duration totalSignatureCacheLookupTime; // std::chrono::high_resolution_clock::duration totalSignatureCacheStoreTime; // std::chrono::high_resolution_clock::duration totalReuseBlocksLookupTime; // std::chrono::high_resolution_clock::duration totalLevelLookupTime; // std::chrono::high_resolution_clock::duration totalBlockEncodingTime; // std::chrono::high_resolution_clock::duration totalMakeNodeTime; }; template SignatureRefiner::SignatureRefiner(storm::dd::DdManager const& manager, storm::expressions::Variable const& blockVariable, std::set const& stateVariables) : manager(&manager), stateVariables(stateVariables) { uint64_t lastStateLevel = 0; for (auto const& stateVariable : stateVariables) { lastStateLevel = std::max(lastStateLevel, manager.getMetaVariable(stateVariable).getHighestLevel()); } internalRefiner = std::make_unique>(manager, blockVariable, lastStateLevel); } template SignatureRefiner::~SignatureRefiner() = default; template Partition SignatureRefiner::refine(Partition const& oldPartition, Signature const& signature) { return internalRefiner->refine(oldPartition, signature); } template class SignatureRefiner; template class SignatureRefiner; template class SignatureRefiner; template class SignatureRefiner; } } }